├── .git-blame-ignore-revs
├── .github
├── FUNDING.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── .rspec
├── .rubocop.yml
├── .yardopts
├── CHANGELOG.md
├── Gemfile
├── README.md
├── Rakefile
├── _config.yml
├── bin
├── console
└── setup
├── docs
├── building_a_rack_application.md
├── composing_applications.md
├── connection_struct.md
├── connection_struct
│ ├── configuring_the_connection_struct.md
│ ├── halting_the_pipe.md
│ └── sharing_data_downstream.md
├── design_model.md
├── dsl_free_usage.md
├── extensions.md
├── extensions
│ ├── container.md
│ ├── cookies.md
│ ├── dry_schema.md
│ ├── flash.md
│ ├── hanami_view.md
│ ├── not_found.md
│ ├── params.md
│ ├── rails.md
│ ├── redirect.md
│ ├── router_params.md
│ ├── session.md
│ └── url.md
├── introduction.md
├── overriding_instance_methods.md
├── plugging_operations.md
├── plugging_operations
│ ├── composing_operations.md
│ ├── injecting_operations.md
│ ├── inspecting_operations.md
│ └── resolving_operations.md
├── plugs.md
├── plugs
│ ├── config.md
│ └── content_type.md
├── recipes
│ ├── hanami_2_and_dry_rb_integration.md
│ ├── hanami_router_integration.md
│ ├── injecting_dependencies_through_dry_auto_inject.md
│ └── using_all_restful_methods.md
├── testing.md
├── using_rack_middlewares.md
└── using_rack_middlewares
│ ├── composing_middlewares.md
│ ├── injecting_middlewares.md
│ └── inspecting_middlewares.md
├── lib
├── web_pipe.rb
└── web_pipe
│ ├── app.rb
│ ├── conn.rb
│ ├── conn_support
│ ├── builder.rb
│ ├── composition.rb
│ ├── errors.rb
│ ├── headers.rb
│ └── types.rb
│ ├── dsl
│ ├── builder.rb
│ ├── class_context.rb
│ └── instance_context.rb
│ ├── extensions
│ ├── container
│ │ └── container.rb
│ ├── cookies
│ │ └── cookies.rb
│ ├── dry_schema
│ │ ├── dry_schema.rb
│ │ └── plugs
│ │ │ └── sanitize_params.rb
│ ├── flash
│ │ └── flash.rb
│ ├── hanami_view
│ │ ├── hanami_view.rb
│ │ └── hanami_view
│ │ │ └── context.rb
│ ├── not_found
│ │ └── not_found.rb
│ ├── params
│ │ ├── params.rb
│ │ └── params
│ │ │ └── transf.rb
│ ├── rails
│ │ └── rails.rb
│ ├── redirect
│ │ └── redirect.rb
│ ├── router_params
│ │ └── router_params.rb
│ ├── session
│ │ └── session.rb
│ └── url
│ │ └── url.rb
│ ├── pipe.rb
│ ├── plug.rb
│ ├── plugs.rb
│ ├── plugs
│ ├── config.rb
│ └── content_type.rb
│ ├── rack_support
│ ├── app_with_middlewares.rb
│ ├── middleware.rb
│ └── middleware_specification.rb
│ ├── test_support.rb
│ ├── types.rb
│ └── version.rb
├── spec
├── dsl
│ ├── composition_spec.rb
│ ├── dry_auto_inject_spec.rb
│ ├── inspecting_middlewares_spec.rb
│ ├── inspecting_operations_spec.rb
│ ├── middleware_composition_spec.rb
│ ├── middleware_injection_spec.rb
│ ├── middleware_spec.rb
│ ├── overriding_instance_methods_spec.rb
│ ├── plug_chaining_spec.rb
│ ├── plug_composition_spec.rb
│ ├── plug_from_block_spec.rb
│ ├── plug_from_method_spec.rb
│ ├── plug_halting_spec.rb
│ ├── plug_injection_spec.rb
│ └── rack_spec.rb
├── extensions
│ ├── container
│ │ └── container_spec.rb
│ ├── cookies
│ │ └── cookies_spec.rb
│ ├── dry_schema
│ │ ├── dry_schema_spec.rb
│ │ └── plugs
│ │ │ └── sanitize_params_spec.rb
│ ├── flash
│ │ ├── flash_spec.rb
│ │ └── integration
│ │ │ └── flash_spec.rb
│ ├── hanami_view
│ │ ├── fixtures
│ │ │ ├── template_with_context.html.str
│ │ │ ├── template_with_input.html.str
│ │ │ └── template_without_input.html.str
│ │ └── hanami_view_spec.rb
│ ├── not_found
│ │ └── not_found_spec.rb
│ ├── params
│ │ ├── params
│ │ │ └── transf_spec.rb
│ │ └── params_spec.rb
│ ├── rails
│ │ └── rails_spec.rb
│ ├── redirect
│ │ └── redirect_spec.rb
│ ├── router_params
│ │ └── router_params_spec.rb
│ ├── session
│ │ └── session_spec.rb
│ └── url
│ │ └── url_spec.rb
├── spec_helper.rb
├── support
│ ├── conn.rb
│ └── middlewares.rb
├── unit
│ └── web_pipe
│ │ ├── app_spec.rb
│ │ ├── conn_spec.rb
│ │ ├── conn_support
│ │ ├── builder_spec.rb
│ │ ├── composition_spec.rb
│ │ └── headers_spec.rb
│ │ ├── pipe_spec.rb
│ │ ├── plug_spec.rb
│ │ ├── plugs
│ │ ├── config_spec.rb
│ │ └── content_type_spec.rb
│ │ ├── rack
│ │ └── middleware_specification_spec.rb
│ │ └── test_support_spec.rb
└── web_pipe_spec.rb
└── web_pipe.gemspec
/.git-blame-ignore-revs:
--------------------------------------------------------------------------------
1 | # Use " for Strings and prefer .() over .call
2 | 4a6ebfdcefe5ad0ff95bfb0c4926cf476037c2d8
3 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: waiting-for-dev
2 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | name: Build
8 | runs-on: ubuntu-latest
9 |
10 | strategy:
11 | fail-fast: false
12 | matrix:
13 | ruby: ["3.0", "3.1", "3.2"]
14 |
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v3
18 |
19 | - name: Setup Ruby
20 | uses: ruby/setup-ruby@v1
21 | with:
22 | ruby-version: ${{ matrix.ruby }}
23 | bundler-cache: true
24 |
25 | - name: Rake
26 | run: bundle exec rake
27 |
28 | - name: Archive SimpleCov Report
29 | uses: actions/upload-artifact@v3
30 | with:
31 | name: coverage
32 | path: coverage
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /_yardoc/
4 | /coverage/
5 | /doc/
6 | /pkg/
7 | /spec/reports/
8 | /tmp/
9 |
10 | # rspec failure tracking
11 | .rspec_status
12 |
13 | Gemfile.lock
14 |
15 | Dockerfile
16 | docker-compose.yml
17 | .dockerignore
18 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --format documentation
2 | --color
3 | --require spec_helper
4 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | AllCops:
2 | TargetRubyVersion: 3.0
3 | NewCops: enable
4 | SuggestExtensions: false
5 | Exclude:
6 | - vendor/**/*
7 |
8 | Metrics/BlockLength:
9 | Exclude:
10 | - spec/**/*
11 | - web_pipe.gemspec
12 |
13 | Naming/AccessorMethodName:
14 | Enabled: false
15 |
16 | Style/HashConversion:
17 | Enabled: false
18 |
19 | Style/StringLiterals:
20 | EnforcedStyle: double_quotes
21 |
22 | Style/LambdaCall:
23 | EnforcedStyle: braces
24 |
--------------------------------------------------------------------------------
/.yardopts:
--------------------------------------------------------------------------------
1 | --title 'web_pipe'
2 | --embed-mixins
3 | --output doc
4 | --readme README.md
5 | --files CHANGELOG.md
6 | --markup markdown
7 | --markup-provider=redcarpet
8 | lib/**/*.rb
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 | All notable changes to this project will be documented in this file.
3 |
4 | The format is based on [Keep a Changelog](http://keepachangelog.com/)
5 | and this project adheres to [Semantic Versioning](http://semver.org/).
6 |
7 | ## [0.16.0] - 2021-11-07
8 | ### Added
9 | - Extract the DSL as an optional convenience layer and introduce
10 | `WebPipe::Pipe` as top abstraction.
11 | [#47](https://github.com/waiting-for-dev/web_pipe/pull/47)
12 | - Be able to plug anything responding to `#to_proc`.
13 | [#47](https://github.com/waiting-for-dev/web_pipe/pull/47)
14 | - Be able to use anything responding to `#to_middlewares`.
15 | [#47](https://github.com/waiting-for-dev/web_pipe/pull/47)
16 |
17 | ## [0.15.1] - 2021-09-19
18 | ### Added
19 | - `:not_found` extension
20 | [#46](https://github.com/waiting-for-dev/web_pipe/pull/46)
21 |
22 | ## [0.15.0] - 2021-09-12
23 | ### Added
24 | - **BREAKING**. Switch `dry_view` extension with `hanami_view`.
25 | [#45](https://github.com/waiting-for-dev/web_pipe/pull/45)
26 |
27 | ## [0.14.0] - 2021-04-14
28 | ### Added
29 | - Inspecting operations
30 | [#42](https://github.com/waiting-for-dev/web_pipe/pull/42)
31 | - Inspecting middlewares
32 | [#43](https://github.com/waiting-for-dev/web_pipe/pull/43)
33 | - Testing support
34 | [#44](https://github.com/waiting-for-dev/web_pipe/pull/44)
35 |
36 | ## [0.13.0] - 2021-01-15
37 | ### Added
38 | - **BREAKING**. Ruby 2.5 deprecated.
39 | [#40](https://github.com/waiting-for-dev/web_pipe/pull/40)
40 | - Ruby 3.0 supported.
41 | [#41](https://github.com/waiting-for-dev/web_pipe/pull/41)
42 |
43 | ## [0.12.1] - 2019-03-18
44 | ### Fixed
45 | - Update rake to fix security alert
46 |
47 | ## [0.12.0] - 2019-12-30
48 | ### Added
49 | - **BREAKING**. Ruby 2.4 deprecated.
50 | - Ruby 2.7 supported.
51 |
52 | ### Fixed
53 | - Ruby 2.7 argument warnings.
54 | [[#38]](https://github.com/waiting-for-dev/web_pipe/pull/38)
55 |
56 | ## [0.11.0] - 2019-12-28
57 | ### Added
58 | - **BREAKING**. `dry-transformer` (former `transproc`) dependency is now
59 | optional.
60 | [[#37]](https://github.com/waiting-for-dev/web_pipe/pull/37)
61 | - Switch `transproc` dependency to `dry-transformer`.
62 | [[#37]](https://github.com/waiting-for-dev/web_pipe/pull/37)
63 |
64 | ## [0.10.0] - 2019-11-15
65 | ### Added
66 | - `:rails` extension integrating with Ruby On Rails.
67 | [[#36]](https://github.com/waiting-for-dev/web_pipe/pull/36)
68 |
69 | ## [0.9.0] - 2019-08-31
70 | ### Added
71 | - Comprehensive documentation.
72 | [[#35]](https://github.com/waiting-for-dev/web_pipe/pull/35)
73 |
74 | ## [0.8.0] - 2019-08-30
75 | ### Added
76 | - **BREAKING**. Rename `Rack` module to `RackSupport`.
77 | [[#34]](https://github.com/waiting-for-dev/web_pipe/pull/34)
78 |
79 | ## [0.7.0] - 2019-08-27
80 | ### Added
81 | - **BREAKING**. `Conn#config` instead of `Conn#bag` for extension configuration.
82 | [[#29]](https://github.com/waiting-for-dev/web_pipe/pull/29)
83 |
84 | - **BREAKING**. `:params` extension extracted from `:url` extension.
85 | [[#30]](https://github.com/waiting-for-dev/web_pipe/pull/30)
86 |
87 | - **BREAKING**. Router params are extracted as a param transformation.
88 | [[#30]](https://github.com/waiting-for-dev/web_pipe/pull/30)
89 |
90 | - **BREAKING**. Plugs now respond to `.call` instead of `.[]`.
91 | [[#31]](https://github.com/waiting-for-dev/web_pipe/pull/31)
92 |
93 | - **BREAKING**. `:dry-schema` extension has not a default handler.
94 | [[#32]](https://github.com/waiting-for-dev/web_pipe/pull/32)
95 |
96 | - **BREAKING**. `:dry-schema` extension stores output in `#config`.
97 | [[#32]](https://github.com/waiting-for-dev/web_pipe/pull/32)
98 |
99 | - Integration with `transproc` gem to provide any number of params
100 | transformations.
101 | [[#30]](https://github.com/waiting-for-dev/web_pipe/pull/30)
102 |
103 | - `:dry-schema` extension automatically loads `:params` extension.
104 | [[#32]](https://github.com/waiting-for-dev/web_pipe/pull/32)
105 |
106 | ## [0.6.1] - 2019-08-02
107 | ### Fixed
108 | - Fixed support for ruby 2.4.
109 | [[#28]](https://github.com/waiting-for-dev/web_pipe/pull/28)
110 |
111 | ## [0.6.0] - 2019-08-02
112 | ### Added
113 | - **BREAKING**. Rename `put` methods as `add`.
114 | [[#26]](https://github.com/waiting-for-dev/web_pipe/pull/26)
115 |
116 | - **BREAKING**. Rename taint to halt, and clean/dirty to ongoing/halted.
117 | [[#25](https://github.com/waiting-for-dev/web_pipe/pull/25)]
118 |
119 | - **BREAKING**. URL redundant methods need to be loaded from `:url` extension.
120 | [[#24](https://github.com/waiting-for-dev/web_pipe/pull/24)]
121 |
122 | - Merge router params with GET and POST params.
123 | [[#23](https://github.com/waiting-for-dev/web_pipe/pull/23)]
124 |
125 | - Extension integrating rack session.
126 | [[#21](https://github.com/waiting-for-dev/web_pipe/pull/21)]
127 |
128 | - Extension to add/delete cookies.
129 | [[#20](https://github.com/waiting-for-dev/web_pipe/pull/20)] &
130 | [[#22](https://github.com/waiting-for-dev/web_pipe/pull/22)]
131 |
132 | - Extension to easily create HTTP redirects.
133 | [[#19](https://github.com/waiting-for-dev/web_pipe/pull/19)]
134 |
135 | - Added `Conn#set_response_headers` method.
136 | [[#27](https://github.com/waiting-for-dev/web_pipe/pull/27)]
137 |
138 |
139 | ## [0.5.0] - 2019-07-26
140 | ### Added
141 | - **BREAKING**. `container` is now an extension.
142 | [[#16](https://github.com/waiting-for-dev/web_pipe/pull/16)]
143 |
144 | - Extension providing Integration with `dry-schema`.
145 | [[#18](https://github.com/waiting-for-dev/web_pipe/pull/18)]
146 |
147 | - No need to manually call `#to_proc` when composing plugs.
148 | [[#13](https://github.com/waiting-for-dev/web_pipe/pull/13)]
149 |
150 | - Extension adding flash functionality to conn.
151 | [[#15](https://github.com/waiting-for-dev/web_pipe/pull/15)]
152 |
153 | - Extensions automatically require their associated plugs, so there is no need
154 | to require them manually anymore.
155 | [[#17](https://github.com/waiting-for-dev/web_pipe/pull/17)]
156 |
157 | ### Fixed
158 | - Fixed bug not allowing middlewares to modify responses initially set with
159 | default values.
160 | [[#14](https://github.com/waiting-for-dev/web_pipe/pull/14)]
161 |
162 |
163 | ## [0.4.0] - 2019-07-17
164 | ### Added
165 | - **BREAKING**. Middlewares have to be named when used.
166 | [[#11](https://github.com/waiting-for-dev/web_pipe/pull/11)]
167 |
168 | - **BREAKING**. Middlewares have to be initialized when composed.
169 | [[#11](https://github.com/waiting-for-dev/web_pipe/pull/11)]
170 |
171 | - **BREAKING**. The array of injected plugs is now scoped within a `plugs:`
172 | kwarg.
173 | [[#11](https://github.com/waiting-for-dev/web_pipe/pull/11)]
174 |
175 | - Middlewares can be injected.
176 | [[#11](https://github.com/waiting-for-dev/web_pipe/pull/11)]
177 |
178 | - DSL helper method `compose` to add middlewares and plugs in order and in a
179 | single shot-
180 | [[#12](https://github.com/waiting-for-dev/web_pipe/pull/11)]
181 |
182 |
183 | ## [0.3.0] - 2019-07-12
184 | ### Added
185 | - **BREAKING**. When plugging with `plug:`, the operation is no longer
186 | specified through `with:`. Now it is just the second positional argument-
187 | [[#9](https://github.com/waiting-for-dev/web_pipe/pull/9)]
188 |
189 | - It is possible to plug a block-
190 | [[#9](https://github.com/waiting-for-dev/web_pipe/pull/9)]
191 |
192 | - WebPipe plug's can be composed. A WebPipe proc representation is the
193 | composition of all its operations, which is an operation itself-
194 | [[#9](https://github.com/waiting-for-dev/web_pipe/pull/9)]
195 |
196 | - WebPipe's middlewares can be composed into another WebPipe class-
197 | [[#10](https://github.com/waiting-for-dev/web_pipe/pull/10)]
198 |
199 |
200 | ## [0.2.0] - 2019-07-05
201 | ### Added
202 | - dry-view integration-
203 | [[#1](https://github.com/waiting-for-dev/web_pipe/pull/1)],
204 | [[#3](https://github.com/waiting-for-dev/web_pipe/pull/3)],
205 | [[#4](https://github.com/waiting-for-dev/web_pipe/pull/4)],
206 | [[#5](https://github.com/waiting-for-dev/web_pipe/pull/5)] &
207 | [[#6](https://github.com/waiting-for-dev/web_pipe/pull/6)]
208 |
209 | - Configuring a container in `WebPipe::Conn`-
210 | [[#2](https://github.com/waiting-for-dev/web_pipe/pull/2)] &
211 | [[#5](https://github.com/waiting-for-dev/web_pipe/pull/5)]
212 |
213 | - Plug to set `Content-Type` response header-
214 | [[#7](https://github.com/waiting-for-dev/web_pipe/pull/7)]
215 |
216 | ### Fixed
217 | - Fix key interpolation in `KeyNotFoundInBagError`-
218 | [[#8](https://github.com/waiting-for-dev/web_pipe/pull/8)]
219 |
220 | ## [0.1.0] - 2019-05-07
221 | ### Added
222 | - Initial release.
223 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source "https://rubygems.org"
4 |
5 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6 |
7 | # Specify your gem's dependencies in web_pipe.gemspec
8 | gemspec
9 |
10 | group :development do
11 | gem "dry-auto_inject", "~> 1.0"
12 | gem "dry-schema", "~> 1.0"
13 | gem "dry-transformer", "~> 0.1"
14 | gem "pry-byebug"
15 | gem "rack-flash3", "~> 1.0"
16 | gem "rack-test", "~> 1.1"
17 | gem "rake", "~> 12.3", ">= 12.3.3"
18 | gem "redcarpet", "~> 3.4"
19 | gem "rspec", "~> 3.0"
20 | gem "rubocop", "~> 1.8"
21 | gem "rubocop-rspec", "~> 2.1"
22 | gem "yard", "~> 0.9", ">= 0.9.20"
23 | # TODO: Move to gemspec when hanami-view 2.0 is available
24 | gem "hanami-view", github: "hanami/view", tag: "v2.1.0.beta2"
25 | end
26 |
27 | group :test do
28 | gem "simplecov", require: false
29 | end
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://badge.fury.io/rb/web_pipe)
2 | [](https://travis-ci.com/waiting-for-dev/web_pipe)
3 |
4 | # WebPipe
5 |
6 | `web_pipe` is a builder of composable rack applications through a pipe of
7 | functions on an immutable struct.
8 |
9 | > `web_pipe` plays incredibly well with `hanami 2`. If you want to create a
10 | > `hanami 2` app with `web_pipe`, you can take inspiration from this sample todo
11 | > application:
12 | >
13 | > https://github.com/waiting-for-dev/hanami_2_web_pipe_todo_app
14 |
15 | 1. [Introduction](docs/introduction.md)
16 | 1. [Design model](docs/design_model.md)
17 | 1. [Building a rack application](docs/building_a_rack_application.md)
18 | 1. [Plugging operations](docs/plugging_operations.md)
19 | 1. [Resolving operations](docs/plugging_operations/resolving_operations.md)
20 | 1. [Injecting operations](docs/plugging_operations/injecting_operations.md)
21 | 1. [Composing operations](docs/plugging_operations/composing_operations.md)
22 | 1. [Inspecting operations](docs/plugging_operations/inspecting_operations.md)
23 | 1. [Using rack middlewares](docs/using_rack_middlewares.md)
24 | 1. [Injecting middlewares](docs/using_rack_middlewares/injecting_middlewares.md)
25 | 1. [Composing middlewares](docs/using_rack_middlewares/composing_middlewares.md)
26 | 1. [Inspecting middlewares](docs/using_rack_middlewares/inspecting_middlewares.md)
27 | 1. [Composing applications](docs/composing_applications.md)
28 | 1. [Connection struct](docs/connection_struct.md)
29 | 1. [Sharing data downstream](docs/connection_struct/sharing_data_downstream.md)
30 | 1. [Halting the pipe](docs/connection_struct/halting_the_pipe.md)
31 | 1. [Configuring the connection struct](docs/connection_struct/configuring_the_connection_struct.md)
32 | 1. [Overriding instance methods](docs/overriding_instance_methods.md)
33 | 1. [DSL free usage](docs/dsl_free_usage.md)
34 | 1. [Plugs](docs/plugs.md)
35 | 1. [Config](docs/plugs/config.md)
36 | 1. [ContentType](docs/plugs/content_type.md)
37 | 1. [Testing](docs/testing.md)
38 | 1. [Extensions](docs/extensions.md)
39 | 1. [Container](docs/extensions/container.md)
40 | 1. [Cookies](docs/extensions/cookies.md)
41 | 1. [Flash](docs/extensions/flash.md)
42 | 1. [Dry Schema](docs/extensions/dry_schema.md)
43 | 1. [Hanami View](docs/extensions/hanami_view.md)
44 | 1. [Not found](docs/extensions/not_found.md)
45 | 1. [Params](docs/extensions/params.md)
46 | 1. [Rails](docs/extensions/rails.md)
47 | 1. [Redirect](docs/extensions/redirect.md)
48 | 1. [Router params](docs/extensions/router_params.md)
49 | 1. [Session](docs/extensions/session.md)
50 | 1. [URL](docs/extensions/url.md)
51 | 1. Recipes
52 | 1. [hanami 2 & dry-rb integration](docs/recipes/hanami_2_and_dry_rb_integration.md)
53 | 1. [Injecting dependencies through dry-auto_inject](docs/recipes/injecting_dependencies_through_dry_auto_inject.md)
54 | 1. [hanami-router integration](docs/recipes/hanami_router_integration.md)
55 | 1. [Using all RESTful methods](docs/recipes/using_all_restful_methods.md)
56 |
57 | ```ruby
58 | # config.ru
59 | require 'web_pipe'
60 |
61 | WebPipe.load_extensions(:params)
62 |
63 | class HelloApp
64 | include WebPipe
65 |
66 | AUTHORIZED_USERS = %w[Alice Joe]
67 |
68 | plug :html
69 | plug :authorize
70 | plug :greet
71 |
72 | private
73 |
74 | def html(conn)
75 | conn.add_response_header('Content-Type', 'text/html')
76 | end
77 |
78 | def authorize(conn)
79 | user = conn.params['user']
80 | if AUTHORIZED_USERS.include?(user)
81 | conn.add(:user, user)
82 | else
83 | conn.
84 | set_status(401).
85 | set_response_body('
Not authorized
').
86 | halt
87 | end
88 | end
89 |
90 | def greet(conn)
91 | conn.set_response_body("Hello #{conn.fetch(:user)}
")
92 | end
93 | end
94 |
95 | run HelloApp.new
96 | ```
97 |
98 | ## Current status
99 |
100 | `web_pipe` is in active development but ready to be used in any environment.
101 | Everyday needs are covered, and while you can expect some API changes,
102 | they won't be essential, and we'll document everything appropriately.
103 |
104 | ## Contributing
105 |
106 | Bug reports and pull requests are welcome on GitHub at
107 | https://github.com/waiting-for-dev/web_pipe.
108 |
109 | ## Release Policy
110 |
111 | `web_pipe` follows the principles of [semantic versioning](http://semver.org/).
112 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "bundler/gem_tasks"
4 | require "rspec/core/rake_task"
5 | require "rubocop/rake_task"
6 |
7 | RSpec::Core::RakeTask.new(:spec)
8 | RuboCop::RakeTask.new
9 |
10 | desc "Run code quality checks"
11 | task lint: %i[rubocop]
12 |
13 | task default: %i[lint spec]
14 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-slate
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require "bundler/setup"
5 | require "web_pipe"
6 |
7 | # You can add fixtures and/or initialization code here to make experimenting
8 | # with your gem easier. You can also use a different console, if you like.
9 |
10 | # (If you use this, don't forget to add pry to your Gemfile!)
11 | # require "pry"
12 | # Pry.start
13 |
14 | require "pry"
15 | Pry.start
16 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 | set -vx
5 |
6 | bundle install
7 |
8 | # Do any other automated setup that you need to do here
9 |
--------------------------------------------------------------------------------
/docs/building_a_rack_application.md:
--------------------------------------------------------------------------------
1 | # Building a rack application
2 |
3 | To build a rack application with `web_pipe`, you have to include
4 | `WebPipe` module in a class:
5 |
6 | ```ruby
7 | require 'web_pipe'
8 |
9 | class MyApp
10 | include WebPipe
11 |
12 | # ...
13 | end
14 | ```
15 |
16 | Then, you can plug the operations and add the rack middlewares you need.
17 |
18 | The instance of that class will be the rack application:
19 |
20 | ```ruby
21 | # config.ru
22 | require 'my_app'
23 |
24 | run MyApp.new
25 | ```
26 |
--------------------------------------------------------------------------------
/docs/composing_applications.md:
--------------------------------------------------------------------------------
1 | # Composing applications
2 |
3 | Previously, we have seen how to [compose plugged
4 | operations](plugging_operations/composing_operations.md) and how to [compose
5 | rack middlewares](using_rack_middlewares/composing_middlewares.md). The logical
6 | next step is composing `web_pipe` applications, which is the same as composing
7 | operations and middlewares simultaneously.
8 |
9 | The DSL method `compose` does precisely that:
10 |
11 | ```ruby
12 | class HtmlApp
13 | include WebPipe
14 |
15 | use :session, Rack::Session::Cookie, key: 'my_app.session', secret: 'long'
16 | use :csrf, Rack::Csrf, raise: true
17 |
18 | plug :content_type
19 | plug :default_status
20 |
21 | private
22 |
23 | def content_type(conn)
24 | conn.add_response_header('Content-Type' => 'text/html')
25 | end
26 |
27 | def default_status(conn)
28 | conn.set_status(404)
29 | end
30 | end
31 |
32 | class MyApp
33 | include WebPipe
34 |
35 | compose :web, HtmlApp.new
36 | # It does exactly the same as:
37 | # use :web, HtmlApp.new
38 | # plug :web, HtmlApp.new
39 |
40 | # use ...
41 | # plug ...
42 | end
43 | ```
44 |
--------------------------------------------------------------------------------
/docs/connection_struct.md:
--------------------------------------------------------------------------------
1 | # Connection struct
2 |
3 | The first operation you plug in a `web_pipe` application receives an instance of
4 | `WebPipe::Conn` automatically created.
5 |
6 | `WebPipe::Conn` is just a struct data type that contains all the information
7 | from the current web request. In this regard, you can think of it as a
8 | structured rack's env hash.
9 |
10 | Request related attributes of this struct are:
11 |
12 | - `#scheme`: `:http` or `:https`.
13 | - `#request_method`: `:get`, `:post`...
14 | - `#host`: e.g. `'www.example.org'`.
15 | - `#ip`: e.g. `'192.168.1.1'`.
16 | - `#port`: e.g. `80` or `443`.
17 | - `#script_name`: e.g. `'index.rb'`.
18 | - `#path_info`: e.g. `'/foor/bar'`.
19 | - `#query_string`: e.g. `'foo=bar&bar=foo'`
20 | - `#request_body`: e.g. `'{ id: 1 }'`
21 | - `#request_headers`: e.g. `{ 'Accept-Charset' => 'utf8' }`
22 | - `#env`: Rack's env hash.
23 | - `#request`: Rack::Request instance.
24 |
25 | Your operations must return another (or the same) instance of the struct, which
26 | will be consumed by the next operation downstream.
27 |
28 | The struct contains methods to add the response data to it:
29 |
30 | - `#set_status(code)`: makes it accessible in the `#status` attribute.
31 | - `#set_response_body(body)`: makes it accessible in the `#response_body`
32 | attribute.
33 | - `#set_response_headers(headers)`: makes them accessible in
34 | the `#response_headers` attribute. Besides, there are also
35 | `#add_response_header(key, value)` and `#delete_response_header(key)`
36 | methods.
37 |
38 | The response in the last struct returned in the pipe will be what is sent to
39 | client.
40 |
41 | Every attribute and method is [fully
42 | documented](https://www.rubydoc.info/github/waiting-for-dev/web_pipe/master/WebPipe/Conn)
43 | in the code documentation.
44 |
45 | Here we have a contrived web application which returns as response body
46 | the request body it has received:
47 |
48 | ```ruby
49 | # config.ru
50 | require 'web_pipe'
51 |
52 | class DummyApp
53 | include WebPipe
54 |
55 | plug :build_response
56 |
57 | private
58 |
59 | def build_response(conn)
60 | conn.
61 | set_status(200).
62 | add_response_header('Content-Type', 'text/html').
63 | set_response_body(
64 | "#{conn.request_body}
"
65 | )
66 | end
67 | end
68 |
69 | run DummyApp.new
70 | ```
71 |
72 | As you can see, by default, the available features are very minimal to read
73 | from a request and to write a response. However, you can pick from several
74 | (extensions)[extensions.md] which will make your life much easier.
75 |
76 | Immutability is a core design principle in `web_pipe`. All methods in
77 | `WebPipe::Conn`, which are used to add data to it (both in core behavior and
78 | extensions), return a fresh new instance. It also makes possible chaining
79 | methods in a very readable way.
80 |
81 | If you're using ruby 2.7 or greater, you can pattern match on a `WebPipe::Conn`
82 | struct, as in:
83 |
84 | ```ruby
85 | # GET http://example.org
86 | conn in { request_method:, host: }
87 | request_method
88 | # :get
89 | host
90 | # 'example.org'
91 | ```
92 |
--------------------------------------------------------------------------------
/docs/connection_struct/configuring_the_connection_struct.md:
--------------------------------------------------------------------------------
1 | # Configuring the connection struct
2 |
3 | [Extensions](../extensions.md) add extra behaviour to the connection struct.
4 | Sometimes they need some user-provided value to work properly or allow some
5 | tweak depending on user needs.
6 |
7 | For this reason, you can add configuration data to a `WebPipe::Conn` instance
8 | so that extensions can fetch it. This shared place where extensions look for
9 | what they need is the `#config` attribute, which is very similar to `#bag`
10 | except for its more private intention.
11 |
12 | In order to interact with `#config`, you can use the method `#add_config(key,
13 | value)` or [`Config` plug](../plugs/config.md).
14 |
15 | ```ruby
16 | class MyApp
17 | include WebPipe
18 |
19 | plug(:config) do |conn|
20 | conn.add_config(
21 | foo: :bar
22 | )
23 | end
24 | end
25 | ```
26 |
--------------------------------------------------------------------------------
/docs/connection_struct/halting_the_pipe.md:
--------------------------------------------------------------------------------
1 | # Halting the pipe
2 |
3 | Each operation in a pipe takes a single `WebPipe::Conn` instance as an argument
4 | and returns another (or the same) instance. A series of operations on the
5 | connection struct is propagated until a final response is sent to the client.
6 |
7 | More often than not, you may need to conditionally stop the propagation of a
8 | pipe at a given operation. For example, you could fetch the user requesting a
9 | resource. In the case they were granted to perform the required action, you
10 | would go on. However, if they weren't, you would like to halt the connection
11 | and respond with a 4xx HTTP status code.
12 |
13 | To stop the pipe, you have to call `#halt` on the connection struct.
14 |
15 | At the implementation level, we must admit that we've not been 100% accurate
16 | until now. We said that the first operation in the pipe received a
17 | `WebPipe::Conn` instance. That's true. However, it is more precise to say that
18 | it gets a `WebPipe::Conn::Ongoing` instance `WebPipe::Conn::Ongoing` being a
19 | subclass of
20 | `WebPipe::Conn`).
21 |
22 | As long as an operation responds with a `WebPipe::Conn::Ongoing`
23 | instance, the propagation will go on. However, when an operation
24 | returns a `WebPipe::Conn::Halted` instance (another subclass of
25 | `WebPipe::Conn`) then any operation downstream will be ignored.
26 | Calling `#halt` copies all attributes to a `WebPipe::Conn::Halted`
27 | instance and returns it.
28 |
29 | This made-up example checks if the user in the request has an admin role. In
30 | that case, it returns the solicited resource. Otherwise, they're unauthorized,
31 | and they never get it.
32 |
33 | ```ruby
34 | WebPipe.load_extensions(:params)
35 |
36 | class ShowTaskApp
37 | include WebPipe
38 |
39 | plug :fetch_user
40 | plug :authorize
41 | plug :render_task
42 |
43 | private
44 |
45 | def fetch_user(conn)
46 | conn.add(
47 | :user, UserRepo.find(conn.params[:user_id])
48 | )
49 | end
50 |
51 | def authorize(conn)
52 | if conn.fetch(:user).admin?
53 | conn
54 | else
55 | conn.
56 | set_status(401).
57 | halt
58 | end
59 | end
60 |
61 | def render_task(conn)
62 | conn.set_response_body(
63 | TaskRepo.find(conn.params[:id]).to_json
64 | )
65 | end
66 | end
67 |
68 | run ShowTaskApp.new
69 | ```
70 |
--------------------------------------------------------------------------------
/docs/connection_struct/sharing_data_downstream.md:
--------------------------------------------------------------------------------
1 | # Sharing data downstream
2 |
3 | Usually, you'll find the need to prepare some data in one operation with the
4 | intention for it to be consumed by another downstream operation. The connection
5 | struct has a `#bag` attribute which is helpful for this purpose.
6 |
7 | `WebPipe::Conn#bag` is a `Hash` with `Symbol` keys where the values can be
8 | anything you need to share. To help with the process, we have the following
9 | methods:
10 |
11 | - `#add(key, value)`: Assigns a value to a key.
12 | - `#fetch(key)`, `#fetch(key, default)`: Retrieves the value associated
13 | to a given key. If it is not found, `default` is returned when
14 | provided.
15 |
16 | This is a simple example of a web application that reads a `name`
17 | parameter and normalizes it before using it in the response body.
18 |
19 | ```ruby
20 | # config.ru
21 | require 'web_pipe'
22 |
23 | WebPipe.load_extensions(:params)
24 |
25 | class NormalizeNameApp
26 | include WebPipe
27 |
28 | plug :normalize_name
29 | plug :respond
30 |
31 | private
32 |
33 | def normalize_name(conn)
34 | conn.add(
35 | :name, conn.params[:name].downcase.capitalize
36 | )
37 | end
38 |
39 | def respond(conn)
40 | conn.set_response_body(
41 | conn.fetch(:name)
42 | )
43 | end
44 | end
45 |
46 | run NormalizeNameApp.new
47 | ```
48 |
--------------------------------------------------------------------------------
/docs/design_model.md:
--------------------------------------------------------------------------------
1 | # Design model
2 |
3 | If you are familiar with rack, you know that it models a two-way pipe. In it,
4 | each middleware has the ability to:
5 |
6 | - During the outbound trip modifying the request as it heads to the actual
7 | application.
8 |
9 | - During the return trip modifying the response as it gets back from the
10 | application.
11 |
12 | ```
13 |
14 | ---------------------> request ----------------------->
15 |
16 | Middleware 1 Middleware 2 Application
17 |
18 | <--------------------- response <-----------------------
19 |
20 |
21 | ```
22 |
23 | `web_pipe` follows a simpler but equally powerful model: a one-way
24 | pipe abstracted on top of rack. A struct that contains data from a
25 | web request is piped through a stack of operations (functions). Each
26 | operation takes as argument an instance of the struct and also
27 | returns an instance of it. You can add response data to the struct at
28 | any moment in the pipe.
29 |
30 | ```
31 |
32 | Operation 1 Operation 2 Operation 3
33 |
34 | --------------------- request/response ---------------->
35 |
36 | ```
37 |
38 | Additionally, any operation in the stack can halt the propagation of the pipe,
39 | leaving downstream operations unexecuted. In this way, the final
40 | response is the one contained in the struct at the moment the pipe was
41 | halted, or the last one if the pipe wasn't halted.
42 |
43 | As you may know, this is the same model used by Elixir's
44 | [`plug`](https://hexdocs.pm/plug/readme.html), from which `web_pipe` takes
45 | inspiration.
46 |
--------------------------------------------------------------------------------
/docs/dsl_free_usage.md:
--------------------------------------------------------------------------------
1 | # DSL free usage
2 |
3 | DSL's (like the one in `web_pipe` with class methods like `plug` or
4 | `use`) provide developers with a user-friendly way to
5 | use a library. However, they usually come at the expense of increasing
6 | complexity in internal code (which sooner than later translates into some
7 | issue).
8 |
9 | `web_pipe` has tried to make an extra effort to minimize these problems. For
10 | this reason, the DSL in this library is just a layer providing convenience on
11 | top of the independent core functionality.
12 |
13 | The DSL methods delegate transparently to instances of `WebPipe::Pipe`, so you
14 | can also work directly with them and forget about magic.
15 |
16 | For instance, the following rack application written through the DSL:
17 |
18 | ```ruby
19 | # config.ru
20 | require 'web_pipe'
21 |
22 | WebPipe.load_extensions(:params)
23 |
24 | class HelloApp
25 | include WebPipe
26 |
27 | plug :fetch_name
28 | plug :render
29 |
30 | private
31 |
32 | def fetch_name(conn)
33 | conn.add(:name, conn.params['name'])
34 | end
35 |
36 | def render(conn)
37 | conn.set_response_body("Hello, #{conn.fetch(:name)}!")
38 | end
39 | end
40 |
41 | run HelloApp.new
42 | ```
43 |
44 | is exactly equivalent to:
45 |
46 | ```ruby
47 | # config.ru
48 | require 'web_pipe'
49 |
50 | WebPipe.load_extensions(:params)
51 |
52 | app = WebPipe::Pipe.new
53 | .plug(:fetch_name, ->(conn) { conn.add(:name, conn.params['name']) })
54 | .plug(:render, ->(conn) { conn.set_response_body("Hello, #{conn.fetch(:name)}") })
55 |
56 | run app
57 | ```
58 |
59 | As you see, the instance of `WebPipe::Pipe` is itself the rack application.
60 |
61 | You can provide a context object to resolve methods when only a name is given
62 | on `#plug`:
63 |
64 | ```ruby
65 | class Context
66 | def fetch_name(conn)
67 | conn.add(:name, conn.params['name'])
68 | end
69 |
70 | def render(conn)
71 | conn.set_response_body("Hello, #{conn.fetch(:name)}")
72 | end
73 | end
74 |
75 | app = WebPipe::Pipe.new(context: Context.new)
76 | .plug(:fetch_name)
77 | .plug(:render)
78 |
79 | run app
80 | ```
81 |
--------------------------------------------------------------------------------
/docs/extensions.md:
--------------------------------------------------------------------------------
1 | # Extensions
2 |
3 | `WebPipe::Conn` features are bare-bones by default: the very minimal you need
4 | to be able to build a web application. However, there are several extensions to
5 | add just the ingredients you want to use progressively.
6 |
7 | To load the extensions, you have to call `#load_extensions` method in
8 | `WebPipe`:
9 |
10 | ```ruby
11 | WebPipe.load_extensions(:params, :cookies)
12 | ```
13 |
--------------------------------------------------------------------------------
/docs/extensions/container.md:
--------------------------------------------------------------------------------
1 | # Container
2 |
3 | `:container` is a simple extension that allows you to configure a dependency
4 | injection container to be accessible from a `WebPipe::Conn` instance.
5 |
6 | The container to use must be configured under the `:container` config key. It
7 | will be accessible through the `#container` method.
8 |
9 | Although you'll usually want to configure the container in the application
10 | class (for instance, using
11 | [dry-system](https://dry-rb.org/gems/dry-system/main/)), having it at the
12 | connection struct level is useful for building other extensions that may need
13 | to access to it, like the [`hanami_view`](hanami_view.md) one.
14 |
15 | ```ruby
16 | require 'web_pipe'
17 | require 'my_container'
18 |
19 | WebPipe.load_extensions(:container)
20 |
21 | class MyApp
22 | plug :config, WebPipe::Plugs::Config.(
23 | container: MyContainer
24 | )
25 | plug :this, :this # Resolved thanks to the container in `include`
26 | plug :that
27 |
28 | private
29 |
30 | def that(conn)
31 | conn.set_response_body(
32 | conn.container['do'].() # Resolved thanks to the container in `:config`
33 | )
34 | end
35 | end
36 | ```
37 |
--------------------------------------------------------------------------------
/docs/extensions/cookies.md:
--------------------------------------------------------------------------------
1 | # Cookies
2 |
3 | Extension helping to deal with request and response cookies.
4 |
5 | Remember, cookies are just the value of `Set-Cookie` header.
6 |
7 | This extension adds following methods:
8 |
9 | - `#request_cookies`: Returns request cookies
10 |
11 | - `#set_cookie(key, value)` or `#set_cookie(key, value, options)`: Instructs
12 | browser to add a new cookie with given key and value.
13 |
14 | Some options can be given as keyword arguments (see [MDN reference on
15 | cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) for an
16 | explanation):
17 |
18 | - `domain:` must be a string.
19 | - `path:` must be a string.
20 | - `max_age:` must be an integer with the number of seconds.
21 | - `expires:` must be a `Time`.
22 | - `secure:` must be `true` or `false`.
23 | - `http_only:` must be `true` or `false`.
24 | - `same_site:` must be one of the symbols `:none`, `:lax` or `:strict`.
25 |
26 | - `#delete_cookie(key)` or `#delete_cookie(key, options)`: Instructs browser to
27 | delete a previously sent cookie.
28 |
29 | Deleting a cookie just means setting again the same key with an expiration
30 | time in the past.
31 |
32 | It accepts `domain:` and `path:` options (see above for a description of
33 | them).
34 |
35 | Example:
36 |
37 | ```ruby
38 | require 'web_pipe'
39 |
40 | WebPipe.load_extensions(:cookies)
41 |
42 | class MyApp
43 | include WebPipe
44 |
45 | plug(:set_cookie) do |conn|
46 | conn.set_cookie('foo', 'bar', secure: true, http_only: true)
47 | end
48 | end
49 | ```
50 |
--------------------------------------------------------------------------------
/docs/extensions/dry_schema.md:
--------------------------------------------------------------------------------
1 | # Dry Schema
2 |
3 | Extension providing integration for every day
4 | [`dry-schema`](https://dry-rb.org/gems/dry-schema/) workflow to validate
5 | parameters.
6 |
7 | A plug `WebPipe::Plugs::SanitizeParams` is added so that you can use it in your
8 | pipe of operations. It takes as arguments a `dry-schema` schema and a handler.
9 | On success, it makes output available at `WebPipe::Conn#sanitized_params`. On
10 | error, it calls the given handler with the connection struct and validation
11 | result.
12 |
13 | This extension automatically loads [`:params` extension](params.md),
14 | as it takes `WebPipe::Conn#params` as input for the validation schema.
15 |
16 | Instead of providing an error handler as the second argument for the plug, you
17 | can configure it under the `:param_sanitization_handler` key. In this way, it
18 | can be reused through composition by other applications.
19 |
20 | ```ruby
21 | require 'db'
22 | require 'dry/schema'
23 | require 'web_pipe'
24 |
25 | WebPipe.load_extensions(:dry_schema)
26 |
27 | class MyApp
28 | include WebPipe
29 |
30 | Schema = Dry::Schema.Params do
31 | required(:name).filled(:string)
32 | end
33 |
34 | plug :config, WebPipe::Plugs::Config.(
35 | param_sanitization_handler: lambda do |conn, result|
36 | conn.
37 | set_status(500).
38 | set_response_body('Error with request parameters').
39 | halt
40 | end
41 | )
42 |
43 | plug :sanitize_params, WebPipe::Plugs::SanitizeParams.(
44 | Schema
45 | )
46 |
47 | plug(:this) do |conn|
48 | DB.persist(:entity, conn.sanitized_params)
49 | end
50 | end
51 | ```
52 |
--------------------------------------------------------------------------------
/docs/extensions/flash.md:
--------------------------------------------------------------------------------
1 | # Flash
2 |
3 | This extension provides the typical flash messages functionality. Messages for
4 | users are stored in session to be consumed by another request after a redirect.
5 |
6 | This extension depends on
7 | [`Rack::Flash`](https://rubygems.org/gems/rack-flash3) (gem name is
8 | `rack-flash3`) and `Rack::Session` (shipped with rack) middlewares.
9 |
10 | `WebPipe::Conn#flash` contains the flash bag. You can use the `#add_flash(key,
11 | value)` method to add a message to it.
12 |
13 | There is also an `#add_flash_now(key, value)` method, which adds a message to
14 | the bag to consume it in the current request. Be aware that it is, in fact,
15 | coupling with the view layer. Something that has to be consumed in the current
16 | request should be just data given to the view layer, but it helps when it can
17 | treat both scenarios as flash messages.
18 |
19 | ```ruby
20 | require 'web_pipe'
21 | require 'rack/session/cookie'
22 | require 'rack-flash'
23 |
24 | WebPipe.load_extensions(:flash)
25 |
26 | class MyApp
27 | include WebPipe
28 |
29 | use :session, Rack::Session::Cookie, secret: 'secret'
30 | use :flash, Rack::Flash
31 |
32 | plug :add_to_flash, ->(conn) { conn.add_flash(:notice, 'Hello world') }
33 |
34 | # Usually you will end up making `conn.flash` available to your view
35 | # system:
36 | #
37 | # <%= flash[:notice] %>
38 | end
39 | ```
40 |
--------------------------------------------------------------------------------
/docs/extensions/hanami_view.md:
--------------------------------------------------------------------------------
1 | # Hanami View
2 |
3 | This extension currently works with `hanami-view` v2.1.0.beta, which is not
4 | still released but available on the gem repository.
5 |
6 | This extension integrates with [hanami-view](https://github.com/hanami/view)
7 | rendering system to set a hanami-view output as the response body.
8 |
9 | `WebPipe::Conn#view` method is at the core of this extension. In its basic
10 | behavior, you provide to it a view instance you want to render and any
11 | exposures or options it may need:
12 |
13 | ```ruby
14 | require 'web_pipe'
15 | require 'hanami/view'
16 | require 'my_context'
17 |
18 | WebPipe.load_extensions(:hanami_view)
19 |
20 | class SayHelloView < Hanami::View
21 | config.paths = [File.join(__dir__, '..', 'templates')]
22 | config.template = 'say_hello'
23 |
24 | expose :name
25 | end
26 |
27 | class MyApp
28 | include WebPipe
29 |
30 | plug :render
31 |
32 | private
33 |
34 | def render(conn)
35 | conn.view(SayHelloView.new, name: 'Joe')
36 | end
37 | end
38 | ```
39 | However, you can resolve a view from a container if you also use the
40 | (`:container` extension)[container.md]:
41 |
42 | ```ruby
43 | require 'hanami_view'
44 | require 'my_container'
45 | require 'web_pipe'
46 |
47 | WebPipe.load_extensions(:hanami_view, :container)
48 |
49 | class MyApp
50 | include WebPipe
51 |
52 | plug :config, WebPipe::Plugs::Config.(
53 | container: MyContainer
54 | )
55 | plug :render
56 |
57 | def render(conn)
58 | conn.view('views.say_hello', name: 'Joe')
59 | end
60 | end
61 | ```
62 |
63 | You can configure the view context class to use through the `:view_context_class` configuration option. The only requirement for it is to implement an initialize method accepting keyword arguments:
64 |
65 | ```ruby
66 | require 'hanami/view'
67 | require 'my_import'
68 |
69 | class MyContext < Hanami::View::Context
70 | def initialize(current_path:)
71 | @current_path = current_path
72 | end
73 | end
74 | ```
75 |
76 | Then, you also need to configure a `:view_context_options` setting, which must be a lambda
77 | accepting a `WebPipe::Conn` instance and returning a hash matching required arguments for
78 | the view context class:
79 |
80 | ```ruby
81 | require 'web_pipe'
82 |
83 | WebPipe.load_extensions(:url)
84 |
85 | class MyApp
86 | include WebPipe
87 |
88 | plug :config, WebPipe::Plugs::Config.(
89 | view_context_class: MyContext,
90 | view_context: ->(conn) { { current_path: conn.full_path} }
91 | )
92 | plug(:render) do |conn|
93 | conn.view(SayHelloView.new, name: 'Joe')
94 | end
95 | end
96 | ```
97 |
--------------------------------------------------------------------------------
/docs/extensions/not_found.md:
--------------------------------------------------------------------------------
1 | # Not found
2 |
3 | This extension helps to build a not-found response in a single method
4 | invocation. The `WebPipe::Conn#not_found` method will:
5 |
6 | - Set 404 as response status.
7 | - Set 'Not found' as the response body, or instead run a step configured in
8 | `:not_found_body_step` config key.
9 | - Halt the connection struct.
10 |
11 | ```ruby
12 | require 'web_pipe'
13 |
14 | WebPipe.load_extensions(:params, :not_found)
15 |
16 | class ShowItem
17 | include 'web_pipe'
18 |
19 | plug :config, WebPipe::Plugs::Config.(
20 | not_found_body_step: ->(conn) { conn.set_response_body('Nothing') }
21 | )
22 |
23 | plug :fetch_item do |conn|
24 | conn.add(:item, Item[params['id']])
25 | end
26 |
27 | plug :check_item do |conn|
28 | if conn.fetch(:item)
29 | conn
30 | else
31 | conn.not_found
32 | end
33 | end
34 |
35 | plug :render do |conn|
36 | conn.set_response_body(conn.fetch(:item).name)
37 | end
38 | end
39 | ```
40 |
--------------------------------------------------------------------------------
/docs/extensions/params.md:
--------------------------------------------------------------------------------
1 | # Params
2 |
3 | This extension adds a `WebPipe::Conn#params` method which returns
4 | request parameters as a Hash. Then, any number of transformations on it can be
5 | configured.
6 |
7 | When no transformations are configured, `#params` returns GET and POST
8 | parameters as a hash:
9 |
10 | ```ruby
11 | # http://www.example.com?foo=bar
12 | conn.params # => { 'foo' => 'bar' }
13 | ```
14 |
15 | You can configure a stack of transformations to be applied to the
16 | parameter hash. For that, we lean on [`dry-transformer`
17 | gem](https://github.com/dry-rb/dry-transformer) (you have to add it yourself to
18 | your Gemfile). All hash transformations in `dry-transformer` are available by
19 | default.
20 |
21 | Transformations must be configured under `:param_transformations`
22 | key:
23 |
24 | ```ruby
25 | require 'web_pipe'
26 |
27 | WebPipe.load_extensions(:params)
28 |
29 | class MyApp
30 | incude WebPipe
31 |
32 | plug :config, WebPipe::Plugs::Config.(
33 | param_transformations: [:deep_symbolize_keys]
34 | )
35 |
36 | plug(:this) do |conn|
37 | # http://www.example.com?foo=bar
38 | conn.params => # => { foo: 'bar' }
39 | # ...
40 | end
41 | end
42 | ```
43 |
44 | Extra needed arguments can be provided as an array:
45 |
46 | ```ruby
47 | # ...
48 | plug :config, WebPipe::Plugs::Config.(
49 | param_transformations: [
50 | :deep_symbolize_keys, [:reject_keys, [:zoo]]
51 | ]
52 | )
53 |
54 | plug(:this) do |conn|
55 | # http://www.example.com?foo=bar&zoo=zoo
56 | conn.params => # => { foo: 'bar' }
57 | # ...
58 | end
59 | # ...
60 | ```
61 |
62 | Custom transformations can be registered in `WebPipe::Params::Transf` `transproc` register:
63 |
64 | ```ruby
65 | fake = ->(_params) { { fake: :params } }
66 | WebPipe::Params::Transf.register(:fake, fake)
67 |
68 | # ...
69 | plug :config, WebPipe::Plugs::Config.(
70 | param_transformations: [:fake]
71 | )
72 |
73 | plug(:this) do |conn|
74 | # http://www.example.com?foo=bar
75 | conn.params => # => { fake: :params }
76 | # ...
77 | end
78 | # ...
79 | ```
80 |
81 | Your own transformation functions can depend on the `WebPipe::Conn`
82 | instance at the moment of calling `#params`. Those functions must accept
83 | the connection struct as its last argument:
84 |
85 | ```ruby
86 | add_name = ->(params, conn) { params.merge(name: conn.fetch(:name)) }
87 | WebPipe::Params::Transf.register(:add_name, add_name)
88 |
89 | # ...
90 | plug :config, WebPipe::Plugs::Config.(
91 | param_transformations: [:deep_symbolize_keys, :add_name]
92 | )
93 |
94 | plug(:add_name) do |conn|
95 | conn.add(:name, 'Alice')
96 | end
97 |
98 | plug(:this) do |conn|
99 | # http://www.example.com?foo=bar
100 | conn.params => # => { foo: :bar, name: 'Alice' }
101 | # ...
102 | end
103 | # ...
104 | ```
105 | Finally, you can override the configured transformations injecting another set
106 | at the moment of calling `#params`:
107 |
108 | ```ruby
109 | # ...
110 | plug :config, WebPipe::Plugs::Config.(
111 | param_transformations: [:deep_symbolize_keys]
112 | )
113 |
114 | plug(:this) do |conn|
115 | # http://www.example.com?foo=bar&zoo=zoo
116 | conn.params([:reject_keys, ['zoo']]) => # => { 'foo' => 'zoo' }
117 | # ...
118 | end
119 | # ...
120 |
--------------------------------------------------------------------------------
/docs/extensions/rails.md:
--------------------------------------------------------------------------------
1 | # Rails
2 |
3 | The first two things to keep in mind to integrate with Rails are
4 | that `WebPipe` instances are Rack applications and that rails router can
5 | perfectly [dispatch to a rack application](https://guides.rubyonrails.org/routing.html#routing-to-rack-applications).
6 |
7 | ```ruby
8 | # config/routes.rb
9 | get '/my_route', to: MyRoute.new
10 |
11 | # app/controllers/my_route.rb
12 | class MyRoute
13 | include WebPipe
14 |
15 | plug :set_response_body
16 |
17 | private
18 |
19 | def set_response_body(conn)
20 | conn.set_response_body('Hello, World!')
21 | end
22 | end
23 | ```
24 |
25 | To do something like the previous example, you don't need to enable this
26 | extension. Notice that rails dispatched the request to our `WebPipe` rack
27 | application, which was then responsible for generating the response. In this
28 | case, it used a simple call to `#set_response_body`.
29 |
30 | It's quite possible that you don't need more than that in terms of rails
31 | integration. Of course, you want something more elaborate to generate
32 | responses. For that, you can use the view or template system you like. One
33 | option that will play especially well here is
34 | [`hanami-view`](https://github.com/hanami/view). Furthermore, we have a
35 | tailored `hanami_view`
36 | [extension](https://waiting-for-dev.github.io/web_pipe/docs/extensions/hanami_view.html).
37 |
38 | You need to use the `:rails` extension if:
39 |
40 | - You want to use `action_view` as a rendering system.
41 | - You want to use rails URL helpers from your `WebPipe` application.
42 | - You want to use controller helpers from your `WebPipe` application.
43 |
44 | Rails' responsibilities for controlling the request/response cycle are coupled
45 | to the rendering process. For this reason, even if you want to use `WebPipe`
46 | applications instead of Rails controller actions you still have to use the
47 | typical top `ApplicationController` to define some behavior for the view layer:
48 |
49 | - Which layout is applied to the template.
50 | - Which helpers will become available to the templates.
51 |
52 | By default, the controller in use is `ActionController::Base`, which means that
53 | no layout is applied and only built-in helpers (for example,
54 | `number_as_currency`) are available. You can change it via the
55 | `:rails_controller` configuration option.
56 |
57 | The main method that this extension adds to `WebPipe::Conn` is `#render`, which
58 | delegates to the [Rails implementation]
59 | (https://api.rubyonrails.org/v6.0.1/classes/ActionController/Renderer.html) as
60 | you'd do in a typical rails controller. Remember that you can provide template
61 | instance variables through the keyword `:assigns`.
62 |
63 | ```ruby
64 | # config/routes.rb
65 | get '/articles', to: ArticlesIndex.new
66 |
67 | # app/controllers/application_controller.rb
68 | class ApplicationController < ActionController::Base
69 | # By default uses the layout in `layouts/application`
70 | end
71 |
72 | # app/controllers/articles_index.rb
73 | WebPipe.load_extensions(:rails) # You can put it in an initializer
74 |
75 | class ArticlesIndex
76 | include WebPipe
77 |
78 | plug :config, WebPipe::Plugs::Config.(
79 | rails_controller: ApplicationController
80 | )
81 |
82 | def render(conn)
83 | conn.render(
84 | template: 'articles/index',
85 | assigns: { articles: Article.all }
86 | )
87 | end
88 | end
89 | ```
90 |
91 | Notice that we used the keyword `template:` instead of taking advantage of
92 | automatic template lookup. We did that way so that we don't have to create an
93 | `ArticlesController`, but it's up to you. In the case of having an
94 | `ArticlesController`, we could do `conn.render(:index, assigns: { articles:
95 | Article.all })`.
96 |
97 | Besides, this extension provides with two other methods:
98 |
99 | - `url_helpers` returns Rails router [url
100 | helpers](https://api.rubyonrails.org/v6.0.1/classes/ActionView/Helpers/UrlHelper.html).
101 | - `helpers` returns the associated [controller
102 | helpers](https://api.rubyonrails.org/classes/ActionController/Helpers.html).
103 |
104 | We have placed `WebPipe` applications within `app/controllers/` directory in
105 | all the examples. However, remember you can put them wherever you like as long
106 | as you respect rails
107 | [`autoload_paths`](https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#autoload-paths).
108 |
--------------------------------------------------------------------------------
/docs/extensions/redirect.md:
--------------------------------------------------------------------------------
1 | # Redirect
2 |
3 | This extension helps create a redirect response.
4 |
5 | Redirect responses consist of two pieces:
6 |
7 | - The `Location` response header with the URL to which browsers should
8 | redirect.
9 | - A 3xx status code.
10 |
11 | A `#redirect(location, code)` method is added to `WebPipe::Conn`, which takes
12 | care of both steps. The `code` argument is optional, defaulting to `302`.
13 |
14 | ```ruby
15 | require 'web_pipe'
16 |
17 | WebPipe.load_extensions(:redirect)
18 |
19 | class MyApp
20 | include WebPipe
21 |
22 | plug(:redirect) do |conn|
23 | conn.redirect('/')
24 | end
25 | end
26 | ```
27 |
--------------------------------------------------------------------------------
/docs/extensions/router_params.md:
--------------------------------------------------------------------------------
1 | # Router params
2 |
3 | This extension can be used to merge placeholder parameters
4 | that usually routers support (like `get /users/:id`) to the parameters hash
5 | added through the [`:params` extension](params.md) (which is
6 | automatically loaded if using `:router_params`).
7 |
8 | This extension adds a transformation function named `:router_params`to the
9 | registry. Internally, it merges what is present in rack env's
10 | `router.params` key.
11 |
12 | It automatically integrates with
13 | [`hanami-router`](https://github.com/hanami/router).
14 |
15 | Don't forget that you have to add yourself the `:router_params`
16 | transformation to the stack.
17 |
18 | ```ruby
19 | require 'web_pipe'
20 |
21 | WebPipe.load_extensions(:router_params)
22 |
23 | class MyApp
24 | include WebPipe
25 |
26 | plug :config, WebPipe::Plugs::Config.(
27 | param_transformations: [:router_params, :deep_symbolize_keys]
28 | )
29 | plug :this
30 |
31 | private
32 |
33 | def this(conn)
34 | # http://example.com/users/1/edit
35 | conn.params # => { id: 1 }
36 | # ...
37 | end
38 | end
39 | ```
40 |
--------------------------------------------------------------------------------
/docs/extensions/session.md:
--------------------------------------------------------------------------------
1 | # Session
2 |
3 | Wrapper around `Rack::Session` middleware to help work with sessions in your
4 | plugged operations.
5 |
6 | It depends on the `Rack::Session` middleware, which is shipped by rack.
7 |
8 | It adds the following methods to `WebPipe::Conn`:
9 |
10 | - `#fetch_session(key)`, `#fetch_session(key, default)` or
11 | `#fetch_session(key) { default }`. Returns what is stored under
12 | given session key. A default value can be given as a second
13 | argument or a block.
14 | - `#add_session(key, value)`. Adds given key/value pair to the
15 | session.
16 | - `#delete_session(key)`. Deletes given key from the session.
17 | - `#clear_session`. Deletes everything from the session.
18 |
19 | ```ruby
20 | require 'web_pipe'
21 | require 'rack/session'
22 |
23 | WebPipe.load_extensions(:session)
24 |
25 | class MyApp
26 | include WebPipe
27 |
28 | use Rack::Session::Cookie, secret: 'top_secret'
29 |
30 | plug(:add_to_session) do |conn|
31 | conn.add_session('foo', 'bar')
32 | end
33 | plug(:fetch_from_session) do |conn|
34 | conn.add(
35 | :foo, conn.fetch_session('foo')
36 | )
37 | end
38 | end
39 | ```
40 |
--------------------------------------------------------------------------------
/docs/extensions/url.md:
--------------------------------------------------------------------------------
1 | # URL
2 |
3 | The `:url` extension adds a few methods that process the raw URL information
4 | into something more digestable.
5 |
6 | Specifically, it adds:
7 |
8 | - `#base_url`: That's schema + host + port (unless it is the default for the scheme). E.g. `'https://example.org'` or `'http://example.org:8000'`.
9 | - `#path`: That's script name (if any) + path information. E.g. `'index.rb/users/1'` or `'users/1'`.
10 | - `#full_path`: That's path + query string (if any). E.g. `'users/1?view=table'`.
11 | - `#url`: That's base url + full path. E.g. `'http://example.org:8000/users/1?view=table'`.
12 |
--------------------------------------------------------------------------------
/docs/introduction.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | `web_pipe` is a rack application builder.
4 |
5 | It means that with it and a rack router (like
6 | [`hanami-router`](https://github.com/hanami/router),
7 | [`http_router`](https://github.com/joshbuddy/http_router) or plain
8 | [rack](https://github.com/rack/rack) routing methods) you can build a complete
9 | web application. However, the idea behind `web_pipe` is for it to be a
10 | decoupled component within a web framework. For this reason, it plays
11 | extremely well with the [hanami](https://hanamirb.org/) &
12 | [dry-rb](https://dry-rb.org/) ecosystems. If it helps, you can think of it as a
13 | decoupled web controller (as the C in MVC).
14 |
15 | `web_pipe` applications are built as a [pipe of
16 | operations](design_model.md) on an [immutable
17 | struct](connection_struct.md). The struct is automatically created
18 | with data from an HTTP request, and it contains methods to
19 | incrementally add data to generate an HTTP response. The pipe can
20 | be [halted](connection_struct/halting_the_pipe.md) at any moment,
21 | taking away from all operations downstream any chance to modify the
22 | response.
23 |
24 | `web_pipe` has a modular design, with only the minimal functionalities needed
25 | to build a web application enabled by default. However, it ships with several
26 | [extensions](extensions.md) to make your life easier.
27 |
28 | Following there is a simple example. It is a web application that will check
29 | the value of a `user` parameter. When it is `Alice` or `Joe`, it will kindly
30 | say hello. Otherwise, it will unauthorize:
31 |
32 | > To try this example, you can paste it to a file with the name `config.ru` and
33 | launch the rack command `rackup` within the same directory. The application
34 | will be available at `http://localhost:9292`.
35 |
36 | ```ruby
37 | require 'web_pipe'
38 |
39 | WebPipe.load_extensions(:params)
40 |
41 | class HelloApp
42 | include WebPipe
43 |
44 | AUTHORIZED_USERS = %w[Alice Joe]
45 |
46 | plug :html
47 | plug :authorize
48 | plug :greet
49 |
50 | private
51 |
52 | def html(conn)
53 | conn.add_response_header('Content-Type', 'text/html')
54 | end
55 |
56 | def authorize(conn)
57 | user = conn.params['user']
58 | if AUTHORIZED_USERS.include?(user)
59 | conn.add(:user, user)
60 | else
61 | conn.
62 | set_status(401).
63 | set_response_body('Not authorized
').
64 | halt
65 | end
66 | end
67 |
68 | def greet(conn)
69 | conn.set_response_body("Hello #{conn.fetch(:user)}
")
70 | end
71 | end
72 |
73 | run HelloApp.new
74 | ```
75 |
--------------------------------------------------------------------------------
/docs/overriding_instance_methods.md:
--------------------------------------------------------------------------------
1 | # Overriding instance methods
2 |
3 | You can override the included instance methods and use `super` to delegate to
4 | the `WebPipe`'s implementation.
5 |
6 | For instance, you might want to add some behavior to your initializer. However,
7 | consider that you need to dispatch the arguments that `WebPipe` needs. Example:
8 |
9 | ```ruby
10 | class MyApp
11 | include WebPipe
12 |
13 | attr_reader :body
14 |
15 | def initialize(body:, **kwargs)
16 | @body = body
17 | super(**kwargs)
18 | end
19 |
20 | plug :render
21 |
22 | private
23 |
24 | def render(conn)
25 | conn.set_response_body(body)
26 | end
27 | end
28 | ```
29 |
30 | The same goes with any other instance method, like Rack's interface:
31 |
32 | ```ruby
33 | class My App
34 | include WebPipe
35 |
36 | plug :render
37 |
38 | def render(conn)
39 | conn.set_response_body(conn.env['body'])
40 | end
41 |
42 | def call(env)
43 | env['body'] = 'Hello, world!'
44 | super
45 | end
46 | end
47 | ```
48 |
--------------------------------------------------------------------------------
/docs/plugging_operations.md:
--------------------------------------------------------------------------------
1 | # Plugging operations
2 |
3 | You can plug operations into your application with the DSL method `plug`. The
4 | first argument it always takes is a symbol with the name you want
5 | to give to the operation (which is needed to allow
6 | [injection](plugging_operations/injecting_operations.md) at
7 | initialization time).
8 |
9 | ```ruby
10 | class MyApp
11 | include WebPipe
12 |
13 | plug :dummy_operation, ->(conn) { conn }
14 | end
15 | ```
16 |
17 | Remember, an operation is just a function (in ruby, anything responding to
18 | `#call`) that takes a struct with connection information and returns another
19 | instance of it. First operation in the stack receives a struct which has been
20 | automatically created with the request data. From then on, any operation can
21 | add to it response data.
22 |
--------------------------------------------------------------------------------
/docs/plugging_operations/composing_operations.md:
--------------------------------------------------------------------------------
1 | # Composing operations
2 |
3 | As we already have said, operations are functions taking a connection struct
4 | and returning a connection struct. As a result, a composition of operations is
5 | an operation in itself (as it also takes a connection struct and returns a
6 | connection struct).
7 |
8 | We can leverage that to plug a whole `web_pipe` application as an operation to
9 | another application. By doing so, you are plugging an operation which is the
10 | composition of all operations for a given application.
11 |
12 | ```ruby
13 | class HtmlApp
14 | include WebPipe
15 |
16 | plug :content_type
17 | plug :default_status
18 |
19 | private
20 |
21 | def content_type(conn)
22 | conn.add_response_header('Content-Type' => 'text/html')
23 | end
24 |
25 | def default_status(conn)
26 | conn.set_status(404)
27 | end
28 | end
29 |
30 | class MyApp
31 | include WebPipe
32 |
33 | plug :html, HtmlApp.new
34 | # plug ...
35 | end
36 | ```
37 |
--------------------------------------------------------------------------------
/docs/plugging_operations/injecting_operations.md:
--------------------------------------------------------------------------------
1 | # Injecting operations
2 |
3 | You can inject operations at the moment an application is initialized. It
4 | allows you to override what the definition declares.
5 |
6 | To this effect, you must use `plugs:` keyword argument. It must be a hash where
7 | the operations are matched by the name you gave them in its definition.
8 |
9 | That is mainly useful for testing purposes, where you can switch a heavy
10 | operation and use another lighter one.
11 |
12 | In the following example, the response body of the application will be
13 | `'Hello from injection'`:
14 |
15 | ```ruby
16 | # config.ru
17 | require 'web_pipe'
18 |
19 | class MyApp
20 | include WebPipe
21 |
22 | plug(:hello) do |conn|
23 | conn.set_response_body('Hello from definition')
24 | end
25 | end
26 |
27 | injection = lambda do |conn|
28 | conn.set_response_body('Hello from injection')
29 | end
30 |
31 | run MyApp.new(plugs: { hello: injection })
32 | ```
33 |
--------------------------------------------------------------------------------
/docs/plugging_operations/inspecting_operations.md:
--------------------------------------------------------------------------------
1 | # Inspecting operations
2 |
3 | Once a `WebPipe` class is initialized, all its operations get resolved. It
4 | happens because they are whether [resolved](resolving_operations.md) or
5 | [injected](injecting_operations.md). The final result can be accessed through
6 | the `#operations` method:
7 |
8 | ```ruby
9 | require 'web_pipe'
10 |
11 | class MyApp
12 | include WebPipe
13 |
14 | plug(:hello) do |conn|
15 | conn.set_response_body('Hello world!')
16 | end
17 | end
18 |
19 | app = MyApp.new
20 | conn = WebPipe::ConnSupport::Builder.call(Rack::MockRequest.env_for)
21 | new_conn = app.operations[:hello].call(con)
22 | conn.response_body #=> ['Hello world!']
23 | ```
24 |
--------------------------------------------------------------------------------
/docs/plugging_operations/resolving_operations.md:
--------------------------------------------------------------------------------
1 | # Resolving operations
2 |
3 | There are several ways you can specify how to resolve an operation.
4 |
5 | ## Instance method
6 |
7 | Operations can be plugged as methods (both public and private) in the
8 | application class:
9 |
10 | ```ruby
11 | class MyApp
12 | include WebPipe
13 |
14 | plug :html
15 |
16 | private
17 |
18 | def html(conn)
19 | conn.add_response_header('Content-Type' => 'text/html')
20 | end
21 | end
22 | ```
23 |
24 | ## `#call`
25 |
26 | Operations can be plugged inline as anything responding to `#call`, like a
27 | `Proc` or a `lambda`:
28 |
29 | ```ruby
30 | class MyApp
31 | include WebPipe
32 |
33 | plug :html, ->(conn) { conn.add_response_header('Content-Type' => 'text/html') }
34 | end
35 | ```
36 |
37 | ## Block
38 |
39 | In the same way that `#call`, operations can also be plugged inline as blocks:
40 |
41 | ```ruby
42 | class MyApp
43 | include WebPipe
44 |
45 | plug :html do |conn|
46 | conn.add_response_header('Content-Type' => 'text/html')
47 | end
48 | end
49 | ```
50 |
--------------------------------------------------------------------------------
/docs/plugs.md:
--------------------------------------------------------------------------------
1 | # Plugs
2 |
3 | Some groups of operations can be generalized as following the same pattern. For
4 | example, an operation setting `Content-Type` header to `text/html` is very
5 | similar to setting the same header to `application/json`. We name plugs to this
6 | level of abstraction on top of operations: plugs are operation builders. In
7 | other words, they are higher-order functions that return functions.
8 |
9 | Being just functions, we take as a convention that plugs respond to `#call` to
10 | create an operation.
11 |
12 | This library ships with some useful plugs.
13 |
--------------------------------------------------------------------------------
/docs/plugs/config.md:
--------------------------------------------------------------------------------
1 | # Config
2 |
3 | The `Config` plug helps in adding configuration settings (`#config` hash
4 | attribute) to an instance of `WebPipe::Conn`.
5 |
6 | ```ruby
7 | require 'web_pipe'
8 |
9 | class MyApp
10 | include WebPipe
11 |
12 | plug :config, WebPipe::Plugs::Config.(
13 | key1: :value1,
14 | key2: :value2
15 | )
16 | end
17 | ```
18 |
--------------------------------------------------------------------------------
/docs/plugs/content_type.md:
--------------------------------------------------------------------------------
1 | # ContentType
2 |
3 | The `ContentType` plug is just a helper to set the `Content-Type` response
4 | header.
5 |
6 | Example:
7 |
8 | ```ruby
9 | require 'web_pipe'
10 |
11 | class MyApp
12 | include WebPipe
13 |
14 | plug :html, WebPipe::Plugs::ContentType.('text/html')
15 | end
16 | ```
17 |
--------------------------------------------------------------------------------
/docs/recipes/hanami_2_and_dry_rb_integration.md:
--------------------------------------------------------------------------------
1 | # Hanami 2 and dry-rb integration
2 |
3 | `web_pipe` has been designed to integrate smoothly with the
4 | [hanami](https://hanamirb.org/) & [dry-rb](https://dry-rb.org/) ecosystems. It
5 | shares the same design principles. It ships with some extensions that even make
6 | this integration painless (like [`:dry-schema`](../extensions/dry_schema.md)
7 | extension or [`:hanami_view`](../extensions/hanami_view.md)), and it seamlessly
8 | [integrates with dry-auto_inject](injecting_dependencies_through_dry_auto_inject.md).
9 |
10 | If you want to use `web_pipe` within a hanami 2 application, you can take
11 | inspiration from this sample todo app:
12 |
13 | https://github.com/waiting-for-dev/hanami_2_web_pipe_todo_app
14 |
--------------------------------------------------------------------------------
/docs/recipes/hanami_router_integration.md:
--------------------------------------------------------------------------------
1 | # hanami-router integration
2 |
3 | A `web_pipe` application instance is a rack application.
4 | Consequently, you can mount it with `hanami-router`'s' `to:`
5 | option.
6 |
7 | ```ruby
8 | # config.ru
9 | require 'hanami/router'
10 | require 'web_pipe'
11 |
12 | class MyApp
13 | include WebPipe
14 |
15 | plug :this, ->(conn) { conn.set_response_body('This') }
16 | end
17 |
18 | router = Hanami::Router.new do
19 | get 'my_app', to: MyApp.new
20 | end
21 |
22 | run router
23 | ```
24 |
25 | To perform [string matching with
26 | variables](https://github.com/hanami/router#string-matching-with-variables) you
27 | just need to load [`:router_params` extension](../extensions/router_params.md).
28 |
--------------------------------------------------------------------------------
/docs/recipes/injecting_dependencies_through_dry_auto_inject.md:
--------------------------------------------------------------------------------
1 | # Injecting dependencies through dry-auto_inject
2 |
3 | `web_pipe` allows injecting [plugs](`../plugging_operations/injecting_operations.md`) and [middlewares](`../using_rack_middlewares/injecting_middlewares.md`) at initialization time. As they are given as keyword arguments to the `#initialize` method, `web_pipe` is only compatible with the [keyword argument strategy from dry-auto_inject](https://dry-rb.org/gems/dry-auto_inject/main/injection-strategies/#keyword-arguments-code-kwargs-code). This is useful in the case you need to use other collaborator from your plugs' definitions.
4 |
5 | ```ruby
6 | WebPipe.load_extensions(:params)
7 |
8 | class CreateUserApp
9 | include WebPipe
10 | include Deps[:create_user]
11 |
12 | plug :html, WebPipe::Plugs::ContentType.('text/html')
13 | plug :create
14 |
15 | private
16 |
17 | def create(conn)
18 | create_user.(conn.params)
19 | end
20 | end
21 | ```
22 |
--------------------------------------------------------------------------------
/docs/recipes/using_all_restful_methods.md:
--------------------------------------------------------------------------------
1 | # Using all RESTful methods
2 |
3 | As you probably know, most browsers don't support some RESTful
4 | methods like `PATCH` or `PUT`. [Rack's `MethodOverride`
5 | middleware](https://github.com/rack/rack/blob/master/lib/rack/method_override.rb)
6 | provides a workaround for this limitation, allowing to override
7 | request method in rack's env if a magical `_method` parameter or
8 | `HTTP_METHOD_OVERRIDE` request header is found.
9 |
10 | You have to be aware that if you use this middleware within a
11 | `web_pipe` application (through [`use` DSL
12 | method](../using_rack_middlewares.md)), it will have no effect.
13 | When your `web_pipe` application takes control of the request, it
14 | has already gone through the router, which is the one that should
15 | read the request method set by rack.
16 |
17 | The solution for this is straightforward. Just use `MethodOverride` middleware
18 | before your router does its work. For example, in `config.ru`:
19 |
20 | ```ruby
21 | # config.ru
22 |
23 | use Rack::MethodOverride
24 |
25 | # Load your router and map to web_pipe applications
26 | ```
27 |
--------------------------------------------------------------------------------
/docs/testing.md:
--------------------------------------------------------------------------------
1 | # Testing
2 |
3 | ## Testing the rack application
4 |
5 | A `WebPipe` instance is a just a rack application, so you can test it as such:
6 |
7 | ```ruby
8 | require 'web_pipe'
9 | require 'rack/mock'
10 | require 'rspec'
11 |
12 | class MyApp
13 | include WebPipe
14 |
15 | plug :response
16 |
17 | private
18 |
19 | def response(conn)
20 | conn
21 | .set_response_body('Hello!')
22 | .set_status(200)
23 | end
24 | end
25 |
26 | RSpec.describe MyApp do
27 | it 'responds with 200 status code' do
28 | env = Rack::MockRequest.env_for
29 |
30 | status, _headers, _body = described_class.new.call(env)
31 |
32 | expect(status).to be(200)
33 | end
34 | end
35 | ```
36 |
37 | ## Testing individual operations
38 |
39 | Each operation in a pipe is an isolated function that takes a connection struct
40 | as argument. You can leverage [the inspection of
41 | operations](docs/plugging_operations/inspecting_operations.md) to unit test them.
42 |
43 | There's also a `WebPipe::TestSupport` module that you can include to get a
44 | helper method `#build_conn` to easily create a connection struct.
45 |
46 | ```ruby
47 | RSpec.describe MyApp do
48 | include WebPipe::TestSupport
49 |
50 | describe '#response' do
51 | it 'responds with 200 status code' do
52 | conn = build_conn
53 | operation = described_class.new.operations[:response]
54 |
55 | new_conn = operation.call(conn)
56 |
57 | expect(new_conn.status).to be(200)
58 | end
59 | end
60 | end
61 | ```
62 |
63 | Check the API documentation for the options you can provide to
64 | [`#build_conn`](https://www.rubydoc.info/github/waiting-for-dev/web_pipe/master/WebPipe/TestSupport#build_conn).
65 |
--------------------------------------------------------------------------------
/docs/using_rack_middlewares.md:
--------------------------------------------------------------------------------
1 | # Using rack middlewares
2 |
3 | A one-way pipe like the one `web_pipe` implements can deal with any required
4 | feature in a web application. However, usually, it is convenient to use some
5 | well-known rack middleware, so you don't have to reinvent the wheel. Even if
6 | you can add them to the router layer, `web_pipe` allows you to encapsulate them
7 | in your application definition.
8 |
9 | To add rack middlewares to the stack, you have to use the DSL method
10 | `use`. The first argument it takes is a `Symbol` with the name you want to
11 | assign to it (which is needed to allow
12 | [injection](using_rack_middlewares/injecting_middlewares.md) on
13 | initialization). Then, it must follow the middleware class and any options it
14 | may need:
15 |
16 | ```ruby
17 | class MyApp
18 | include WebPipe
19 |
20 | use :cookies, Rack::Session::Cookie, key: 'my_app.session', secret: 'long'
21 | end
22 | ```
23 |
--------------------------------------------------------------------------------
/docs/using_rack_middlewares/composing_middlewares.md:
--------------------------------------------------------------------------------
1 | # Composing middlewares
2 |
3 | In a similar way that you compose plugged operations, you can also compose rack
4 | middlewares from another application.
5 |
6 | For that, you just need to `use` another application. All the middlewares for
7 | that application will be added to the stack in the same order.
8 |
9 | ```ruby
10 | class HtmlApp
11 | include WebPipe
12 |
13 | use :session, Rack::Session::Cookie, key: 'my_app.session', secret: 'long'
14 | use :csrf, Rack::Csrf, raise: true
15 | end
16 |
17 | class MyApp
18 | include WebPipe
19 |
20 | use :html, HtmlApp.new
21 | # use ...
22 |
23 | # plug ...
24 | end
25 | ```
26 |
--------------------------------------------------------------------------------
/docs/using_rack_middlewares/injecting_middlewares.md:
--------------------------------------------------------------------------------
1 | # Injecting middlewares
2 |
3 | You can inject middlewares at the moment an application is initialized,
4 | allowing you to override what you have defined in the DSL.
5 |
6 | For that purpose, you have to use the `middlewares:` keyword argument. It must be a
7 | hash where middlewares are matched by the name you gave them in its definition.
8 |
9 | A middleware must be specified as an `Array`. The first item must be a rack
10 | middleware class. The rest of the arguments (if any) should be any options it may
11 | need.
12 |
13 | That is mainly useful for testing purposes, where you can switch a heavy
14 | middleware and use a mocked one instead.
15 |
16 | In the following example, we mock the rack session mechanism:
17 |
18 | ```ruby
19 | # config.ru
20 | require 'web_pipe'
21 | require 'rack/session/cookie'
22 |
23 | class MyApp
24 | include WebPipe
25 |
26 | use :session, Rack::Session::Cookie, key: 'my_app.session', secret: 'long'
27 |
28 | plug(:serialize_session) do |conn|
29 | conn.set_response_body(conn.env['rack.session'].inspect)
30 | end
31 | end
32 |
33 | class MockedSession
34 | attr_reader :app, :key
35 |
36 | def initialize(app, key)
37 | @app = app
38 | @key = key
39 | end
40 |
41 | def call(env)
42 | env['rack.session'] = "Mocked for '#{key}' key"
43 | end
44 | end
45 |
46 | run MyApp.new(middlewares: { session: [MockedSession, 'my_app_mocked'] })
47 | ```
48 |
--------------------------------------------------------------------------------
/docs/using_rack_middlewares/inspecting_middlewares.md:
--------------------------------------------------------------------------------
1 | # Inspecting middlewares
2 |
3 | Once a `WebPipe` class is initialized, all its middlewares get resolved. You
4 | can access them through the `#middlewares` method.
5 |
6 | Each middleware is represented by a
7 | `WebPipe::RackSupport::MiddlewareSpecification` instance, which contains two
8 | accessors: `middleware` returns the middleware class. In contrast, `options`
9 | returns an array with the arguments provided to the middleware on
10 | initialization.
11 |
12 | Keep in mind that every middleware is resolved as an array. That is because it
13 | can be composed by a chain of middlewares built through
14 | [composition](composing_middlewares.md).
15 |
16 |
17 | ```ruby
18 | require 'web_pipe'
19 | require 'rack/session'
20 |
21 | class MyApp
22 | include WebPipe
23 |
24 | use :session, Rack::Session::Cookie, key: 'my_app.session', secret: 'long'
25 |
26 | plug(:hello) do |conn|
27 | conn.set_response_body('Hello world!')
28 | end
29 | end
30 |
31 | app = MyApp.new
32 | session_middleware = app.middlewares[:session][0]
33 | session_middleware.middleware # => Rack::Session::Cookie
34 | session_middleware.options # => [{ key: 'my_app.session', secret: 'long' }]
35 | ```
36 |
--------------------------------------------------------------------------------
/lib/web_pipe.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dry/core/extensions"
4 | require "zeitwerk"
5 |
6 | # Entry-point for the DSL layer.
7 | #
8 | # Including this module into your class adds to it a DSL layer which makes it
9 | # convenient to interact with an instance of {WebPipe::Pipe} transparently. It
10 | # means that the DSL is actually an optional layer, and you can achieve
11 | # everything by using {WebPipe::Pipe} instances.
12 | #
13 | # Your class gets access to {WebPipe::DSL::ClassContext::DSL_METHODS} at the
14 | # class level, while {WebPipe::DSL::InstanceContext::PIPE_METHODS} are available
15 | # for every instance of it. Both groups of methods are delegating to
16 | # {WebPipe::Pipe}, so you can look there for documentation.
17 | #
18 | # @example
19 | # class HelloWorld
20 | # include WebPipe
21 | #
22 | # use :runtime, Rack::Runtime
23 | #
24 | # plug :content_type do |conn|
25 | # conn.add_response_header('Content-Type', 'plain/text')
26 | # end
27 | #
28 | # plug :render do |conn|
29 | # conn.set_response_body('Hello, World!')
30 | # end
31 | # end
32 | #
33 | # The instance of your class is itself the final rack application. When you
34 | # initialize it, you have the chance to inject different plugs or middlewares
35 | # from those defined at the class level.
36 | #
37 | # @example
38 | # HelloWorld.new(
39 | # middlewares: {
40 | # runtime: [Class.new do
41 | # def initialize(app)
42 | # @app = app
43 | # end
44 | #
45 | # def call(env)
46 | # status, headers, body = @app.call(env)
47 | # [status, headers.merge('Injected' => '1'), body]
48 | # end
49 | # end]
50 | # },
51 | # plugs: {
52 | # render: ->(conn) { conn.set_response_body('Injected!') }
53 | # }
54 | # )
55 | module WebPipe
56 | def self.loader
57 | Zeitwerk::Loader.for_gem.tap do |loader|
58 | loader.ignore(
59 | "#{__dir__}/web_pipe/conn_support/errors.rb",
60 | "#{__dir__}/web_pipe/extensions"
61 | )
62 | loader.inflector.inflect("dsl" => "DSL")
63 | end
64 | end
65 | loader.setup
66 |
67 | extend Dry::Core::Extensions
68 |
69 | # Called via {Module#include}, makes available web_pipe's DSL.
70 | #
71 | # Includes an instance of `Builder`. That means that `Builder#included` is
72 | # eventually called.
73 | def self.included(klass)
74 | klass.include(DSL::Builder.new)
75 | end
76 |
77 | register_extension :container do
78 | require "web_pipe/extensions/container/container"
79 | end
80 |
81 | register_extension :cookies do
82 | require "web_pipe/extensions/cookies/cookies"
83 | end
84 |
85 | register_extension :dry_schema do
86 | require "web_pipe/extensions/dry_schema/dry_schema"
87 | require "web_pipe/extensions/dry_schema/plugs/sanitize_params"
88 | end
89 |
90 | register_extension :flash do
91 | require "web_pipe/extensions/flash/flash"
92 | end
93 |
94 | register_extension :hanami_view do
95 | require "web_pipe/extensions/hanami_view/hanami_view"
96 | end
97 |
98 | register_extension :not_found do
99 | require "web_pipe/extensions/not_found/not_found"
100 | end
101 |
102 | register_extension :params do
103 | require "web_pipe/extensions/params/params"
104 | end
105 |
106 | register_extension :rails do
107 | require "web_pipe/extensions/rails/rails"
108 | end
109 |
110 | register_extension :redirect do
111 | require "web_pipe/extensions/redirect/redirect"
112 | end
113 |
114 | register_extension :router_params do
115 | require "web_pipe/extensions/router_params/router_params"
116 | end
117 |
118 | register_extension :session do
119 | require "web_pipe/extensions/session/session"
120 | end
121 |
122 | register_extension :url do
123 | require "web_pipe/extensions/url/url"
124 | end
125 | end
126 |
--------------------------------------------------------------------------------
/lib/web_pipe/app.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dry/monads"
4 |
5 | module WebPipe
6 | # Rack app built from a chain of functions that take and return a
7 | # {WebPipe::Conn}.
8 | #
9 | # This is the abstraction encompassing a rack application built only with the
10 | # functions on {WebPipe::Conn}. {WebPipe::RackSupport::AppWithMiddlewares}
11 | # takes middlewares also into account.
12 | #
13 | # A rack application is something callable that takes the rack environment as
14 | # an argument, and returns a rack response. So, this class needs to:
15 | #
16 | # - Take rack's environment and create a {WebPipe::Conn} struct from there.
17 | # - Starting from the initial struct, apply the pipe of functions.
18 | # - Convert the last {WebPipe::Conn} back to a rack response.
19 | #
20 | # {WebPipe::Conn} can itself be of two different types (subclasses of it}:
21 | # {Conn::Ongoing} and {Conn::Halted}. The pipe is stopped on two scenarios:
22 | #
23 | # - The end of the pipe is reached.
24 | # - One function returns a {Conn::Halted}.
25 | class App
26 | include Dry::Monads::Result::Mixin
27 |
28 | # @!attribute [r] operations
29 | # @return [Array]
30 | attr_reader :operations
31 |
32 | # @param operations [Array]
33 | def initialize(operations)
34 | @operations = operations
35 | end
36 |
37 | # @param env [Hash] Rack environment
38 | #
39 | # @return env [Array] Rack response
40 | # @raise ConnSupport::Composition::InvalidOperationResult when an
41 | # operation doesn't return a {WebPipe::Conn}
42 | def call(env)
43 | extract_rack_response(
44 | apply_operations(
45 | conn_from_env(
46 | env
47 | )
48 | )
49 | )
50 | end
51 |
52 | private
53 |
54 | def conn_from_env(env)
55 | ConnSupport::Builder.(env)
56 | end
57 |
58 | def apply_operations(conn)
59 | ConnSupport::Composition.new(operations).(conn)
60 | end
61 |
62 | def extract_rack_response(conn)
63 | conn.rack_response
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/lib/web_pipe/conn.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dry/struct"
4 | require "web_pipe/conn_support/errors"
5 |
6 | module WebPipe
7 | # Struct and methods about web request and response data.
8 | #
9 | # It is meant to contain all the data coming from a web request
10 | # along with all the data needed to build a web response. It can
11 | # be built with {ConnSupport::Builder}.
12 | #
13 | # Besides data fetching methods and {#rack_response}, any other
14 | # method returns a fresh new instance of it, so it is thought to
15 | # be used in an immutable way and to allow chaining of method
16 | # calls.
17 | #
18 | # There are two subclasses (two types) for this:
19 | # {Conn::Ongoing} and {Conn::Halted}. {ConnSupport::Builder} constructs
20 | # a {Conn::Ongoing} struct, while {#halt} copies the data to a
21 | # {Conn::Halted} instance. The intention of this is to halt
22 | # operations on the web request/response cycle one a {Conn::Halted}
23 | # instance is detected.
24 | #
25 | # @example
26 | # WebPipe::ConnSupport::Builder.call(env).
27 | # set_status(404).
28 | # add_response_header('Content-Type', 'text/plain').
29 | # set_response_body('Not found').
30 | # halt
31 | class Conn < Dry::Struct
32 | include ConnSupport::Types
33 |
34 | # @!attribute [r] env
35 | #
36 | # Rack env hash.
37 | #
38 | # @return [Env[]]
39 | #
40 | # @see https://www.rubydoc.info/github/rack/rack/file/SPEC
41 | attribute :env, Env
42 |
43 | # @!attribute [r] request
44 | #
45 | # Rack request.
46 | #
47 | # @return [Request[]]
48 | #
49 | # @see https://www.rubydoc.info/github/rack/rack/Rack/Request
50 | attribute :request, Request
51 |
52 | # @!attribute [r] scheme
53 | #
54 | # Scheme of the request.
55 | #
56 | # @return [Scheme[]]
57 | #
58 | # @example
59 | # :http
60 | attribute :scheme, Scheme
61 |
62 | # @!attribute [r] request_method
63 | #
64 | # Method of the request.
65 | #
66 | # It is not called `:method` in order not to collide with
67 | # {Object#method}.
68 | #
69 | # @return [Method[]]
70 | #
71 | # @example
72 | # :get
73 | attribute :request_method, Method
74 |
75 | # @!attribute [r] host
76 | #
77 | # Host being requested.
78 | #
79 | # @return [Host[]]
80 | #
81 | # @example
82 | # 'www.example.org'
83 | attribute :host, Host
84 |
85 | # @!attribute [r] ip
86 | #
87 | # IP being requested.
88 | #
89 | # @return [IP[]]
90 | #
91 | # @example
92 | # '192.168.1.1'
93 | attribute :ip, Ip
94 |
95 | # @!attribute [r] port
96 | #
97 | # Port in which the request is made.
98 | #
99 | # @return [Port[]]
100 | #
101 | # @example
102 | # 443
103 | attribute :port, Port
104 |
105 | # @!attribute [r] script_name
106 | #
107 | # Script name in the URL, or the empty string if none.
108 | #
109 | # @return [ScriptName[]]
110 | #
111 | # @example
112 | # 'index.rb'
113 | attribute :script_name, ScriptName
114 |
115 | # @!attribute [r] path_info
116 | #
117 | # Besides {#script_name}, the remainder path of the URL or the
118 | # empty string if none. It is, at least, `/` when `#script_name`
119 | # is empty.
120 | #
121 | # This doesn't include the {#query_string}.
122 | #
123 | # @return [PathInfo[]]
124 | #
125 | # @example
126 | # '/foo/bar'.
127 | attribute :path_info, PathInfo
128 |
129 | # @!attribute [r] query_string
130 | #
131 | # Query String of the URL (everything after `?` , or the empty
132 | # string if none).
133 | #
134 | # @return [QueryString[]]
135 | #
136 | # @example
137 | # 'foo=bar&bar=foo'
138 | attribute :query_string, QueryString
139 |
140 | # @!attribute [r] request_body
141 | #
142 | # Body sent by the request.
143 | #
144 | # @return [RequestBody[]]
145 | #
146 | # @example
147 | # '{ resource: "foo" }'
148 | attribute :request_body, RequestBody
149 |
150 | # @!attribute [r] request_headers
151 | #
152 | # Hash of request headers.
153 | #
154 | # As per RFC2616, headers names are case insensitive. Here, they
155 | # are normalized to PascalCase acting on dashes ('-').
156 | #
157 | # Notice that when a rack server maps headers to CGI-like
158 | # variables, both dashes and underscores (`_`) are treated as
159 | # dashes. Here, they always remain as dashes.
160 | #
161 | # @return [Headers[]]
162 | #
163 | # @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
164 | #
165 | # @example
166 | # { 'Accept-Charset' => 'utf8' }
167 | attribute :request_headers, Headers
168 |
169 | # @!attribute [r] status
170 | #
171 | # Status sent by the response.
172 | #
173 | # @return [Status[]]
174 | #
175 | # @example
176 | # 200
177 | attribute :status, Status
178 |
179 | # @!attribute [r] response_body
180 | #
181 | # @return [ResponseBody[]] Body sent by the response.
182 | #
183 | # @example
184 | # ['']
185 | attribute :response_body, ResponseBody
186 |
187 | # @!attribute [r] response_headers
188 | #
189 | # Response headers.
190 | #
191 | # @see #request_headers for normalization details
192 | #
193 | # @return [Headers[]]
194 | #
195 | # @example
196 | #
197 | # { 'Content-Type' => 'text/html' }
198 | attribute :response_headers, Headers
199 |
200 | # @!attribute [r] bag
201 | #
202 | # Hash where anything can be stored. Keys
203 | # must be symbols.
204 | #
205 | # This can be used to store anything that is needed to be
206 | # consumed downstream in a pipe of operations action on and
207 | # returning {Conn}.
208 | #
209 | # @return [Bag[]]
210 | attribute :bag, Bag
211 |
212 | # @!attribute [r] config
213 | #
214 | # Instance level configuration.
215 | #
216 | # It is a hash where anything can be stored. Keys must be symbols.
217 | #
218 | # The idea of it is for extensions to have somewhere to store user
219 | # provided values they may need at some point.
220 | #
221 | # @return [Bag[]]
222 | attribute :config, Bag
223 |
224 | # Sets response status code.
225 | #
226 | # @param code [StatusCode]
227 | #
228 | # @return [Conn]
229 | def set_status(code)
230 | new(
231 | status: code
232 | )
233 | end
234 |
235 | # Sets response body.
236 | #
237 | # As per rack specification, the response body must respond to
238 | # `#each`. Here, when given `content` responds to `:each` it is
239 | # set as it is as the new response body. Otherwise, what is set
240 | # is a one item array of it.
241 | #
242 | # @param content [#each, String]
243 | #
244 | # @return [Conn]
245 | #
246 | # @see https://www.rubydoc.info/github/rack/rack/master/file/SPEC#label-The+Body
247 | def set_response_body(content)
248 | new(
249 | response_body: content.respond_to?(:each) ? content : [content]
250 | )
251 | end
252 |
253 | # Sets response headers.
254 | #
255 | # Substitues everything that was present as response headers by
256 | # the new given hash.
257 | #
258 | # Headers keys are normalized.
259 | #
260 | # @param headers [Hash]
261 | #
262 | # @return [Conn]
263 | #
264 | # @see ConnSupport::Headers.normalize_key
265 | def set_response_headers(headers)
266 | new(
267 | response_headers: ConnSupport::Headers.normalize(headers)
268 | )
269 | end
270 |
271 | # Adds given pair to response headers.
272 | #
273 | # `key` is normalized.
274 | #
275 | # @param key [String]
276 | # @param value [String]
277 | #
278 | # @return [Conn]
279 | #
280 | # @see ConnSupport::Headers.normalize_key
281 | def add_response_header(key, value)
282 | new(
283 | response_headers: ConnSupport::Headers.add(
284 | response_headers, key, value
285 | )
286 | )
287 | end
288 |
289 | # Deletes pair with given key from response headers.
290 | #
291 | # It accepts a non normalized key.
292 | #
293 | # @param key [String]
294 | #
295 | # @return [Conn]
296 | #
297 | # @see ConnSupport::Headers.normalize_key
298 | def delete_response_header(key)
299 | new(
300 | response_headers: ConnSupport::Headers.delete(
301 | response_headers, key
302 | )
303 | )
304 | end
305 |
306 | # Reads an item from {#bag}.
307 | #
308 | # @param key [Symbol]
309 | #
310 | # @return [Object]
311 | #
312 | # @raise ConnSupport::KeyNotFoundInBagError when key is not
313 | # registered in the bag.
314 | def fetch(key, default = Types::Undefined)
315 | return bag.fetch(key, default) unless default == Types::Undefined
316 |
317 | bag.fetch(key) { raise ConnSupport::KeyNotFoundInBagError, key }
318 | end
319 |
320 | # Writes an item to the {#bag}.
321 | #
322 | # If it already exists, it is overwritten.
323 | #
324 | # @param key [Symbol]
325 | # @param value [Object]
326 | #
327 | # @return [Conn]
328 | def add(key, value)
329 | new(
330 | bag: bag.merge(key => value)
331 | )
332 | end
333 |
334 | # Reads an item from {#config}.
335 | #
336 | # @param key [Symbol]
337 | #
338 | # @return [Object]
339 | #
340 | # @raise ConnSupport::KeyNotFoundInConfigError when key is not
341 | # present in {#config}.
342 | def fetch_config(key, default = Types::Undefined)
343 | return config.fetch(key, default) unless default == Types::Undefined
344 |
345 | config.fetch(key) { raise ConnSupport::KeyNotFoundInConfigError, key }
346 | end
347 |
348 | # Writes an item to {#config}.
349 | #
350 | # If it already exists, it is overwritten.
351 | #
352 | # @param key [Symbol]
353 | # @param value [Object]
354 | #
355 | # @return [Conn]
356 | def add_config(key, value)
357 | new(
358 | config: config.merge(key => value)
359 | )
360 | end
361 |
362 | # Builds response in the way rack expects.
363 | #
364 | # It is useful to finish a rack application built with a
365 | # {Conn}. After every desired operation has been done,
366 | # this method has to be called before giving control back to
367 | # rack.
368 | #
369 | # @return
370 | # [Array]
371 | #
372 | # @api private
373 | def rack_response
374 | [
375 | status,
376 | response_headers,
377 | response_body
378 | ]
379 | end
380 |
381 | # Copies all the data to a {Halted} instance and
382 | # returns it.
383 | #
384 | # @return [Halted]
385 | def halt
386 | Halted.new(attributes)
387 | end
388 |
389 | # Returns whether the instance is {Halted}.
390 | #
391 | # @return [Bool]
392 | def halted?
393 | is_a?(Halted)
394 | end
395 |
396 | # Type of {Conn} representing an ongoing request/response
397 | # cycle.
398 | class Ongoing < Conn; end
399 |
400 | # Type of {Conn} representing a halted request/response
401 | # cycle.
402 | class Halted < Conn; end
403 | end
404 | end
405 |
--------------------------------------------------------------------------------
/lib/web_pipe/conn_support/builder.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rack"
4 |
5 | module WebPipe
6 | module ConnSupport
7 | # @api private
8 | module Builder
9 | # rubocop:disable Metrics/MethodLength
10 | def self.call(env)
11 | rr = Rack::Request.new(env)
12 | Conn::Ongoing.new(
13 | request: rr,
14 | env: env,
15 | scheme: rr.scheme.to_sym,
16 | request_method: rr.request_method.downcase.to_sym,
17 | host: rr.host,
18 | ip: rr.ip,
19 | port: rr.port,
20 | script_name: rr.script_name,
21 | path_info: rr.path_info,
22 | query_string: rr.query_string,
23 | request_body: rr.body,
24 | request_headers: Headers.extract(env)
25 | )
26 | end
27 | # rubocop:enable Metrics/MethodLength
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/web_pipe/conn_support/composition.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dry/monads"
4 |
5 | module WebPipe
6 | module ConnSupport
7 | # @api private
8 | class Composition
9 | # Raised when operation doesn't return a {WebPipe::Conn}.
10 | class InvalidOperationResult < RuntimeError
11 | def initialize(returned)
12 | super(
13 | <<~MSG
14 | An operation returned +#{returned.inspect}+. To be valid,
15 | an operation must return whether a
16 | WebPipe::Conn::Ongoing or a WebPipe::Conn::Halted.
17 | MSG
18 | )
19 | end
20 | end
21 |
22 | include Dry::Monads[:result]
23 |
24 | attr_reader :operations
25 |
26 | def initialize(operations)
27 | @operations = operations
28 | end
29 |
30 | def call(conn)
31 | extract_result(
32 | apply_operations(
33 | conn
34 | )
35 | )
36 | end
37 |
38 | private
39 |
40 | def apply_operations(conn)
41 | operations.reduce(Success(conn)) do |new_conn, operation|
42 | new_conn.bind { |c| apply_operation(c, operation) }
43 | end
44 | end
45 |
46 | def apply_operation(conn, operation)
47 | result = operation.(conn)
48 | case result
49 | when Conn::Ongoing
50 | Success(result)
51 | when Conn::Halted
52 | Failure(result)
53 | else
54 | raise InvalidOperationResult, result
55 | end
56 | end
57 |
58 | def extract_result(result)
59 | extract_proc = :itself.to_proc
60 |
61 | result.either(extract_proc, extract_proc)
62 | end
63 | end
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/lib/web_pipe/conn_support/errors.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module WebPipe
4 | module ConnSupport
5 | # Error raised when trying to fetch an entry in {WebPipe::Conn#bag} for an
6 | # unknown key.
7 | class KeyNotFoundInBagError < KeyError
8 | # @param key [Any] Key not found in the bag
9 | def initialize(key)
10 | super(
11 | <<~MSG
12 | Bag does not contain a key with name +#{key}+.
13 | MSG
14 | )
15 | end
16 | end
17 |
18 | # Error raised when trying to fetch an entry in {WebPipeConn#config} for an
19 | # unknown key.
20 | class KeyNotFoundInConfigError < KeyError
21 | # @param key [Any] Key not found in config
22 | def initialize(key)
23 | super(
24 | <<~MSG
25 | Config does not contain a key with name +#{key}+.
26 | MSG
27 | )
28 | end
29 | end
30 |
31 | # Error raised when trying to use a {WebPipe::Conn} feature which requires a
32 | # rack middleware that is not present
33 | class MissingMiddlewareError < RuntimeError
34 | # @param feature [String] Name of the feature intended to be used
35 | # @param middleware [String] Name of the missing middleware
36 | # @param gem [String] Gem name for the middleware
37 | def initialize(feature, middleware, gem)
38 | super(
39 | <<~MSG
40 | In order to use #{feature} you must use #{middleware} middleware:
41 | https://rubygems.org/gems/#{gem}
42 | MSG
43 | )
44 | end
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/lib/web_pipe/conn_support/headers.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module WebPipe
4 | module ConnSupport
5 | # @api private
6 | module Headers
7 | # Headers which come as plain CGI-like variables (without the `HTTP_`
8 | # prefixed) from the rack server.
9 | HEADERS_AS_CGI = %w[CONTENT_TYPE CONTENT_LENGTH].freeze
10 |
11 | # Headers are all those pairs which key begins with `HTTP_` plus
12 | # those detailed in {HEADERS_AS_CGI}.
13 | def self.extract(env)
14 | Hash[
15 | env
16 | .select { |k, _v| k.start_with?("HTTP_") }
17 | .map { |k, v| pair(k[5..], v) }
18 | .concat(
19 | env
20 | .select { |k, _v| HEADERS_AS_CGI.include?(k) }
21 | .map { |k, v| pair(k, v) }
22 | )
23 | ]
24 | end
25 |
26 | def self.add(headers, key, value)
27 | Hash[
28 | headers.to_a.push(pair(key, value))
29 | ]
30 | end
31 |
32 | def self.delete(headers, key)
33 | headers.reject { |k, _v| normalize_key(key) == k }
34 | end
35 |
36 | def self.pair(key, value)
37 | [normalize_key(key), value]
38 | end
39 |
40 | # As per RFC2616, headers names are case insensitive. This
41 | # function normalizes them to PascalCase acting on dashes ('-').
42 | #
43 | # When a rack server maps headers to CGI-like variables, both
44 | # dashes and underscores (`_`) are treated as dashes. This
45 | # function substitutes all '-' to '_'.
46 | #
47 | # See https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
48 | def self.normalize_key(key)
49 | key.downcase.gsub("_", "-").split("-").map(&:capitalize).join("-")
50 | end
51 |
52 | def self.normalize(headers)
53 | headers.transform_keys { |k| normalize_key(k) }
54 | end
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/lib/web_pipe/conn_support/types.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dry/types"
4 | require "rack/request"
5 |
6 | module WebPipe
7 | module ConnSupport
8 | # Types used in the {WebPipe::Conn} struct.
9 | #
10 | # The implementation self-describes them, but you can look at the
11 | # {WebPipe::Conn} attributes for documentation.
12 | module Types
13 | include Dry.Types()
14 |
15 | Env = Strict::Hash
16 | Request = Instance(Rack::Request)
17 |
18 | Scheme = Strict::Symbol.enum(:http, :https)
19 | Method = Strict::Symbol.enum(
20 | :get, :head, :post, :put, :delete, :connect, :options, :trace, :patch
21 | )
22 | Host = Strict::String
23 | Ip = Strict::String.optional
24 | Port = Strict::Integer
25 | ScriptName = Strict::String
26 | PathInfo = Strict::String
27 | QueryString = Strict::String
28 | RequestBody = Interface(:gets, :each, :read, :rewind)
29 |
30 | Status = Strict::Integer
31 | .default(200)
32 | .constrained(gteq: 100, lteq: 599)
33 | ResponseBody = Interface(:each).default { [""] }
34 |
35 | Headers = Strict::Hash
36 | .map(Strict::String, Strict::String)
37 | .default { {} }
38 |
39 | Bag = Strict::Hash
40 | .map(Strict::Symbol, Strict::Any)
41 | .default { {} }
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/web_pipe/dsl/builder.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module WebPipe
4 | module DSL
5 | # @api private
6 | class Builder < Module
7 | attr_reader :class_context, :instance_context
8 |
9 | def initialize
10 | @class_context = ClassContext.new
11 | @instance_context = InstanceContext.new(
12 | class_context: class_context
13 | )
14 | super()
15 | end
16 |
17 | def included(klass)
18 | klass.extend(class_context)
19 | klass.include(instance_context)
20 | end
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/web_pipe/dsl/class_context.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module WebPipe
4 | module DSL
5 | # @api private
6 | class ClassContext < Module
7 | DSL_METHODS = %i[use plug compose].freeze
8 |
9 | attr_reader :ast
10 |
11 | def initialize
12 | @ast = []
13 | super
14 | end
15 |
16 | def extended(klass)
17 | define_dsl_methods(klass, ast)
18 | end
19 |
20 | private
21 |
22 | def define_dsl_methods(klass, ast)
23 | DSL_METHODS.each do |method|
24 | klass.define_singleton_method(method) do |*args, **kwargs, &block|
25 | ast << [method, args, kwargs, block]
26 | end
27 | end
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/web_pipe/dsl/instance_context.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module WebPipe
4 | module DSL
5 | # @api private
6 | class InstanceContext < Module
7 | PIPE_METHODS = %i[
8 | call middlewares operations to_proc to_middlewares
9 | ].freeze
10 |
11 | attr_reader :class_context
12 |
13 | def initialize(class_context:)
14 | @class_context = class_context
15 | super()
16 | end
17 |
18 | def included(klass)
19 | klass.include(dynamic_module(class_context.ast))
20 | end
21 |
22 | private
23 |
24 | def dynamic_module(ast)
25 | Module.new.tap do |mod|
26 | define_initialize(mod, ast)
27 | define_pipe_methods(mod)
28 | end
29 | end
30 |
31 | # rubocop:disable Metrics/MethodLength
32 | def define_initialize(mod, ast)
33 | mod.define_method(:initialize) do |plugs: {}, middlewares: {}, **kwargs|
34 | super(**kwargs) # Compatibility with dry-auto_inject
35 | acc = Pipe.new(context: self)
36 | @pipe = ast.reduce(acc) do |pipe, node|
37 | method, args, kwargs, block = node
38 | if block
39 | pipe.send(method, *args, **kwargs, &block)
40 | else
41 | pipe.send(method, *args, **kwargs)
42 | end
43 | end.inject(plugs: plugs, middleware_specifications: middlewares)
44 | end
45 | end
46 | # rubocop:enable Metrics/MethodLength
47 |
48 | def define_pipe_methods(mod)
49 | PIPE_METHODS.each do |method|
50 | mod.define_method(method) do |*args|
51 | @pipe.send(method, *args)
52 | end
53 | end
54 | end
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/lib/web_pipe/extensions/container/container.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # :nodoc:
4 | module WebPipe
5 | # See the docs for the extension linked from the README.
6 | module Container
7 | # Returns {Conn#config} `:container` value
8 | #
9 | # @return [Any]
10 | def container
11 | fetch_config(:container)
12 | end
13 | end
14 |
15 | Conn.include(Container)
16 | end
17 |
--------------------------------------------------------------------------------
/lib/web_pipe/extensions/cookies/cookies.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rack/utils"
4 |
5 | # :nodoc:
6 | module WebPipe
7 | # See the docs for the extension linked from the README.
8 | module Cookies
9 | # Valid options for {#set_cookie}.
10 | SET_COOKIE_OPTIONS = Types::Strict::Hash.schema(
11 | domain?: Types::Strict::String.optional,
12 | path?: Types::Strict::String.optional,
13 | max_age?: Types::Strict::Integer.optional,
14 | expires?: Types::Strict::Time.optional,
15 | secure?: Types::Strict::Bool.optional,
16 | http_only?: Types::Strict::Bool.optional,
17 | same_site?: Types::Strict::Symbol.enum(:none, :lax, :strict).optional
18 | )
19 |
20 | # Valid options for {#delete_cookie}.
21 | DELETE_COOKIE_OPTIONS = Types::Strict::Hash.schema(
22 | domain?: Types::Strict::String.optional,
23 | path?: Types::Strict::String.optional
24 | )
25 |
26 | # @return [Hash]
27 | def request_cookies
28 | request.cookies
29 | end
30 |
31 | # @param key [String]
32 | # @param value [String]
33 | # @param opts [SET_COOKIE_OPTIONS[]]
34 | def set_cookie(key, value, opts = Types::EMPTY_HASH)
35 | Rack::Utils.set_cookie_header!(
36 | response_headers,
37 | key,
38 | { value: value }.merge(SET_COOKIE_OPTIONS[opts])
39 | )
40 | self
41 | end
42 |
43 | # @param key [String]
44 | # @param opts [DELETE_COOKIE_OPTIONS[]]
45 | def delete_cookie(key, opts = Types::EMPTY_HASH)
46 | Rack::Utils.delete_cookie_header!(
47 | response_headers,
48 | key,
49 | DELETE_COOKIE_OPTIONS[opts]
50 | )
51 | self
52 | end
53 | end
54 |
55 | Conn.include(Cookies)
56 | end
57 |
--------------------------------------------------------------------------------
/lib/web_pipe/extensions/dry_schema/dry_schema.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | WebPipe.load_extensions(:params)
4 |
5 | # :nodoc:
6 | module WebPipe
7 | # See the docs for the extension linked from the README.
8 | module DrySchema
9 | SANITIZED_PARAMS_KEY = :sanitized_params
10 |
11 | def sanitized_params
12 | fetch_config(SANITIZED_PARAMS_KEY)
13 | end
14 | end
15 |
16 | Conn.include(DrySchema)
17 | end
18 |
--------------------------------------------------------------------------------
/lib/web_pipe/extensions/dry_schema/plugs/sanitize_params.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "web_pipe/extensions/dry_schema/dry_schema"
4 |
5 | module WebPipe
6 | module Plugs
7 | # Sanitize {Conn#params} with given `dry-schema` Schema.
8 | #
9 | # @see WebPipe::DrySchema
10 | module SanitizeParams
11 | # {Conn#config} key to store the handler.
12 | #
13 | # @return [Symbol]
14 | PARAM_SANITIZATION_HANDLER_KEY = :param_sanitization_handler
15 |
16 | # @param schema [Dry::Schema::Processor]
17 | # @param handler [ParamSanitizationHandler::Handler[]]
18 | #
19 | # @return [ConnSupport::Composition::Operation[], Types::Undefined]
20 | def self.call(schema, handler = Types::Undefined)
21 | lambda do |conn|
22 | result = schema.(conn.params)
23 | if result.success?
24 | conn.add_config(DrySchema::SANITIZED_PARAMS_KEY, result.output)
25 | else
26 | get_handler(conn, handler).(conn, result)
27 | end
28 | end
29 | end
30 |
31 | def self.get_handler(conn, handler)
32 | return handler unless handler == Types::Undefined
33 |
34 | conn.fetch_config(PARAM_SANITIZATION_HANDLER_KEY)
35 | end
36 | private_class_method :get_handler
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/web_pipe/extensions/flash/flash.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "web_pipe/conn_support/errors"
4 |
5 | # :nodoc:
6 | module WebPipe
7 | # See the docs for the extension linked from the README.
8 | module Flash
9 | RACK_FLASH_KEY = "x-rack.flash"
10 |
11 | # Returns the flash bag.
12 | #
13 | # @return [Rack::Flash::FlashHash]
14 | #
15 | # @raises ConnSupport::MissingMiddlewareError when `Rack::Flash`
16 | # is not being used as middleware
17 | def flash
18 | env.fetch(RACK_FLASH_KEY) do
19 | raise ConnSupport::MissingMiddlewareError.new(
20 | "flash", "Rack::Flash", "https://rubygems.org/gems/rack-flash3"
21 | )
22 | end
23 | end
24 |
25 | # Adds an item to the flash bag to be consumed by next request.
26 | #
27 | # @param key [String]
28 | # @param value [String]
29 | def add_flash(key, value)
30 | flash[key] = value
31 | self
32 | end
33 |
34 | # Adds an item to the flash bag to be consumed by the same request
35 | # in process.
36 | #
37 | # @param key [String]
38 | # @param value [String]
39 | def add_flash_now(key, value)
40 | flash.now[key] = value
41 | self
42 | end
43 | end
44 |
45 | Conn.include(Flash)
46 | end
47 |
--------------------------------------------------------------------------------
/lib/web_pipe/extensions/hanami_view/hanami_view.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "web_pipe/extensions/hanami_view/hanami_view/context"
4 | require "hanami/view"
5 |
6 | # :nodoc:
7 | module WebPipe
8 | # See the docs for the extension linked in the README.
9 | module HanamiView
10 | VIEW_CONTEXT_CLASS_KEY = :view_context_class
11 | private_constant :VIEW_CONTEXT_CLASS_KEY
12 |
13 | DEFAULT_VIEW_CONTEXT_CLASS = Class.new(WebPipe::HanamiView::Context)
14 | private_constant :DEFAULT_VIEW_CONTEXT_CLASS
15 |
16 | VIEW_CONTEXT_OPTIONS_KEY = :view_context_options
17 | private_constant :VIEW_CONTEXT_OPTIONS_KEY
18 |
19 | DEFAULT_VIEW_CONTEXT_OPTIONS = ->(_conn) { {} }
20 |
21 | # Sets string output of a view as response body.
22 | #
23 | # If the view is not a {Hanami::View} instance, it is resolved from
24 | # the configured container.
25 | #
26 | # `kwargs` is used as the input for the view (the arguments that
27 | # {Hanami::View#call} receives).
28 | #
29 | # @param view_spec [Hanami::View, Any]
30 | # @param kwargs [Hash] Arguments to pass along to `Hanami::View#call`
31 | #
32 | # @return WebPipe::Conn
33 | def view(view_spec, **kwargs)
34 | view_instance = view_instance(view_spec)
35 |
36 | set_response_body(
37 | view_instance.(**view_input(kwargs)).to_str
38 | )
39 | end
40 |
41 | private
42 |
43 | def view_instance(view_spec)
44 | return view_spec if view_spec.is_a?(Hanami::View)
45 |
46 | fetch_config(:container)[view_spec]
47 | end
48 |
49 | def view_input(kwargs)
50 | return kwargs if kwargs.key?(:context)
51 |
52 | context = fetch_config(
53 | VIEW_CONTEXT_CLASS_KEY, DEFAULT_VIEW_CONTEXT_CLASS
54 | ).new(
55 | **fetch_config(
56 | VIEW_CONTEXT_OPTIONS_KEY, DEFAULT_VIEW_CONTEXT_OPTIONS
57 | ).(self)
58 | )
59 | kwargs.merge(context: context)
60 | end
61 | end
62 |
63 | Conn.include(HanamiView)
64 | end
65 |
--------------------------------------------------------------------------------
/lib/web_pipe/extensions/hanami_view/hanami_view/context.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "hanami/view"
4 |
5 | module WebPipe
6 | module HanamiView
7 | # Noop context class for Hanami::View used by default.
8 | class Context < Hanami::View::Context
9 | def initialize(**_kwargs)
10 | super()
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/web_pipe/extensions/not_found/not_found.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # :nodoc:
4 | module WebPipe
5 | # See the docs for the extension linked from the README.
6 | module NotFound
7 | # @api private
8 | RESPONSE_BODY_STEP_CONFIG_KEY = :not_found_body_step
9 |
10 | # Generates the not-found response
11 | #
12 | # @return [WebPipe::Conn::Halted]
13 | # @see NotFound
14 | def not_found
15 | set_status(404)
16 | .then do |conn|
17 | response_body_step = conn.fetch_config(RESPONSE_BODY_STEP_CONFIG_KEY,
18 | ->(c) { c.set_response_body("Not found") })
19 |
20 | response_body_step.(conn)
21 | end.halt
22 | end
23 |
24 | Conn.include(NotFound)
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/web_pipe/extensions/params/params.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "web_pipe/extensions/params/params/transf"
4 |
5 | # :nodoc:
6 | module WebPipe
7 | # See the docs for the extension linked from the README.
8 | module Params
9 | # Key where configured transformations are set
10 | PARAM_TRANSFORMATION_KEY = :param_transformations
11 |
12 | # @param transformation_specs [Array, Types::Undefined]
13 | # @return [Any]
14 | def params(transformation_specs = Types::Undefined)
15 | specs = if transformation_specs == Types::Undefined
16 | fetch_config(PARAM_TRANSFORMATION_KEY, [])
17 | else
18 | transformation_specs
19 | end
20 | transformations = specs.reduce(Transf[:id]) do |acc, t|
21 | acc >> transformation(t)
22 | end
23 |
24 | Transf[transformations].(request.params)
25 | end
26 |
27 | private
28 |
29 | def transformation(spec)
30 | transformation = Transf[*spec]
31 | if (transformation.fn.arity - transformation.args.count) == 1
32 | transformation
33 | else
34 | Transf[spec, self]
35 | end
36 | end
37 | end
38 |
39 | Conn.include(Params)
40 | end
41 |
--------------------------------------------------------------------------------
/lib/web_pipe/extensions/params/params/transf.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dry/transformer"
4 |
5 | module WebPipe
6 | module Params
7 | # Parameter transformations from dry-transformer.
8 | module Transf
9 | extend Dry::Transformer::Registry
10 |
11 | import Dry::Transformer::HashTransformations
12 |
13 | def self.id(params)
14 | params
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/web_pipe/extensions/rails/rails.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # :nodoc:
4 | module WebPipe
5 | # See the docs for the extension linked from the README.
6 | module Rails
7 | def render(*args)
8 | set_response_body(
9 | rails_controller.renderer.render(*args)
10 | )
11 | end
12 |
13 | # @see https://devdocs.io/rails~6.0/actioncontroller/helpers
14 | def helpers
15 | rails_controller.helpers
16 | end
17 |
18 | # @see https://api.rubyonrails.org/v6.0.1/classes/ActionView/Helpers/UrlHelper.html
19 | def url_helpers
20 | ::Rails.application.routes.url_helpers
21 | end
22 |
23 | private
24 |
25 | def rails_controller
26 | config.fetch(:rails_controller, ActionController::Base)
27 | end
28 | end
29 |
30 | Conn.include(Rails)
31 | end
32 |
--------------------------------------------------------------------------------
/lib/web_pipe/extensions/redirect/redirect.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # :nodoc:
4 | module WebPipe
5 | # See the docs for the extension linked from the README.
6 | module Redirect
7 | # Location header
8 | LOCATION_HEADER = "Location"
9 |
10 | # Valid type for a redirect status code
11 | RedirectCode = Types::Strict::Integer.constrained(gteq: 300, lteq: 399)
12 |
13 | # @param location [String]
14 | # @param code [Integer]
15 | def redirect(location, code = 302)
16 | add_response_header(LOCATION_HEADER, location)
17 | .set_status(RedirectCode[code])
18 | end
19 | end
20 |
21 | Conn.include(Redirect)
22 | end
23 |
--------------------------------------------------------------------------------
/lib/web_pipe/extensions/router_params/router_params.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | WebPipe.load_extensions(:params)
4 |
5 | module WebPipe
6 | # See the docs for the extension linked from the README.
7 | module RouterParams
8 | ROUTER_PARAM_KEY = "router.params"
9 |
10 | # @param params [Hash]
11 | # @param conn [WebPipe::Conn]
12 | #
13 | # @return [Hash]
14 | def self.call(params, conn)
15 | params.merge(conn.env.fetch(ROUTER_PARAM_KEY, Types::EMPTY_HASH))
16 | end
17 |
18 | WebPipe::Params::Transf.register(:router_params, method(:call))
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/web_pipe/extensions/session/session.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rack"
4 |
5 | # :nodoc:
6 | module WebPipe
7 | # See the docs for the extension linked from the README.
8 | module Session
9 | # Type for session keys.
10 | SESSION_KEY = Types::Strict::String
11 |
12 | # Returns Rack::Session's hash
13 | #
14 | # @return [Rack::Session::Abstract::SessionHash]
15 | def session
16 | env.fetch(Rack::RACK_SESSION) do
17 | raise ConnSupport::MissingMiddlewareError.new(
18 | "session", "Rack::Session", "https://www.rubydoc.info/github/rack/rack/Rack/Session"
19 | )
20 | end
21 | end
22 |
23 | # Fetches given key from the session.
24 | #
25 | # @param key [SESSION_KEY[]] Session key to fetch
26 | # @param default [Any] Default value if key is not found
27 | # @yieldreturn Default value if key is not found and default is not given
28 | # @raise KeyError When key is not found and not default nor block are given
29 | # @return [Any]
30 | def fetch_session(*args, &block)
31 | SESSION_KEY[args[0]]
32 | session.fetch(*args, &block)
33 | end
34 |
35 | # Adds given key/value pair to the session.
36 | #
37 | # @param key [SESSION_KEY[]] Session key
38 | # @param value [Any] Value
39 | # @return [Conn]
40 | def add_session(key, value)
41 | session[SESSION_KEY[key]] = value
42 | self
43 | end
44 |
45 | # Deletes given key form the session.
46 | #
47 | # @param key [SESSION_KEY[]] Session key
48 | # @return [Conn]
49 | def delete_session(key)
50 | session.delete(SESSION_KEY[key])
51 | self
52 | end
53 |
54 | # Deletes everything from the session.
55 | #
56 | # @return [Conn]
57 | def clear_session
58 | session.clear
59 | self
60 | end
61 | end
62 |
63 | Conn.include(Session)
64 | end
65 |
--------------------------------------------------------------------------------
/lib/web_pipe/extensions/url/url.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # :nodoc:
4 | module WebPipe
5 | # See the docs for the extension linked from the README.
6 | module Url
7 | # Base part of the URL.
8 | #
9 | # This is {#scheme} and {#host}, adding {#port} unless it is the
10 | # default one for the scheme.
11 | #
12 | # @return [String]
13 | #
14 | # @example
15 | # 'https://example.org'
16 | # 'http://example.org:8000'
17 | def base_url
18 | request.base_url
19 | end
20 |
21 | # URL path.
22 | #
23 | # This is {#script_name} and {#path_info}.
24 | #
25 | # @return [String]
26 | #
27 | # @example
28 | # 'index.rb/users'
29 | def path
30 | request.path
31 | end
32 |
33 | # URL full path.
34 | #
35 | # This is {#path} with {#query_string} if present.
36 | #
37 | # @return [String]
38 | #
39 | # @example
40 | # '/users?id=1'
41 | def full_path
42 | request.fullpath
43 | end
44 |
45 | # Request URL.
46 | #
47 | # This is the same as {#base_url} plus {#full_path}.
48 | #
49 | # @return [String]
50 | #
51 | # @example
52 | # 'http://www.example.org:8000/users?id=1'
53 | def url
54 | request.url
55 | end
56 | end
57 |
58 | Conn.include(Url)
59 | end
60 |
--------------------------------------------------------------------------------
/lib/web_pipe/pipe.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module WebPipe
4 | # Composable rack application builder.
5 | #
6 | # An instance of this class helps build rack applications that can compose.
7 | # Besides the DSL, which only adds a convenience layer, this is the higher
8 | # abstraction on the library.
9 | #
10 | # Applications are built by plugging functions that take and return a
11 | # {WebPipe::Conn} instance. That's an immutable struct that contains all the
12 | # request information alongside methods to build the response. See {#plug} for
13 | # details.
14 | #
15 | # Middlewares can also be added to the resulting application thanks to {#use}.
16 | #
17 | # Be aware that instances of this class are immutable, so methods return new
18 | # objects every time.
19 | #
20 | # The instance itself is the final rack application.
21 | #
22 | # @example
23 | # # config.ru
24 | # app = WebPipe::Pipe.new
25 | # .use(:runtime, Rack::Runtime)
26 | # .plug(:content_type) do |conn|
27 | # conn.add_response_header('Content-Type', 'text/plain')
28 | # end
29 | # .plug(:render) do |conn|
30 | # conn.set_response_body('Hello, World!')
31 | # end
32 | #
33 | # run app
34 | class Pipe
35 | # @!attribute [r] context
36 | # Object from where resolve operations. See {#plug}.
37 | attr_reader :context
38 |
39 | # @api private
40 | EMPTY_PLUGS = [].freeze
41 |
42 | # @api private
43 | EMPTY_MIDDLEWARE_SPECIFICATIONS = [].freeze
44 |
45 | # @api private
46 | attr_reader :plugs
47 |
48 | # @api private
49 | attr_reader :middleware_specifications
50 |
51 | # @param context [Any] Object from where resolve plug's operations (see
52 | # {#plug})
53 | def initialize(
54 | context: nil,
55 | plugs: EMPTY_PLUGS,
56 | middleware_specifications: EMPTY_MIDDLEWARE_SPECIFICATIONS
57 | )
58 | @plugs = plugs
59 | @middleware_specifications = middleware_specifications
60 | @context = context
61 | end
62 |
63 | # Names and adds a plug operation to the application.
64 | #
65 | # The operation can be provided in several ways:
66 | #
67 | # - Through the `spec` parameter as:
68 | # - Anything responding to `#call` (like a {Proc}).
69 | # - Anything responding to `#to_proc` (like another {WebPipe::Pipe}
70 | # instance or an instance of a class including {WebPipe}).
71 | # - As `nil` (default), meaning that the operation is a method in
72 | # {#context} matching the `name` parameter.
73 | # - Through a block, if the `spec` parameter is `nil`.
74 | #
75 | # @param name [Symbol]
76 | # @param spec [#call, #to_proc, String, Symbol, nil]
77 | # @yieldparam [WebPipe::Conn]
78 | #
79 | # @return [WebPipe::Pipe] A fresh new instance with the added plug.
80 | def plug(name, spec = nil, &block_spec)
81 | with(
82 | plugs: [
83 | *plugs,
84 | Plug.new(name: name, spec: spec || block_spec)
85 | ]
86 | )
87 | end
88 |
89 | # Names and adds a rack middleware to the final application.
90 | #
91 | # The middleware can be given in three forms:
92 | #
93 | # - As one or two arguments, the first one being a
94 | # rack middleware class, and optionally a second one with its initialization
95 | # options.
96 | # - As something responding to `#to_middlewares` with an array of
97 | # {WebPipe::RackSupport::Middleware} (like another {WebPipe::Pipe} instance
98 | # or a class including {WebPipe}), case in which all middlewares are used.
99 | #
100 | # @overload use(name, middleware_class)
101 | # @param name [Symbol]
102 | # @param middleware_class [Class]
103 | # @overload use(name, middleware_class, middleware_options)
104 | # @param name [Symbol]
105 | # @param middleware_class [Class]
106 | # @param middleware_options [Any]
107 | # @overload use(name, to_middlewares)
108 | # @param name [Symbol]
109 | # @param middleware_class [#to_middlewares]
110 | #
111 | # @return [WebPipe::Pipe] A fresh new instance with the added middleware.
112 | def use(name, *spec)
113 | with(
114 | middleware_specifications: [
115 | *middleware_specifications,
116 | RackSupport::MiddlewareSpecification.new(name: name, spec: spec)
117 | ]
118 | )
119 | end
120 |
121 | # Shortcut for {#plug} and {#use} a pipe at once.
122 | #
123 | # @param name [#to_sym]
124 | # @param spec [#to_proc#to_middlewares]
125 | def compose(name, spec)
126 | use(name, spec)
127 | .plug(name, spec)
128 | end
129 |
130 | # Operations {#plug}ged to the app, mapped by their names.
131 | #
132 | # @return [Hash{Symbol => Proc}]
133 | def operations
134 | @operations ||= Hash[
135 | plugs.map { |plug| [plug.name, plug.(context)] }
136 | ]
137 | end
138 |
139 | # Middlewares {#use}d in the app, mapped by their names.
140 | #
141 | # Returns them wrapped within {WebPipe::RackSupport::Middleware} instances,
142 | # from where you can access their classes and options.
143 | #
144 | # @return [Hash{Symbol=>Array}]
145 | def middlewares
146 | @middlewares ||= Hash[
147 | middleware_specifications.map { |mw_spec| [mw_spec.name, mw_spec.()] }
148 | ]
149 | end
150 |
151 | # @api private
152 | def to_proc
153 | ConnSupport::Composition
154 | .new(operations.values)
155 | .method(:call)
156 | end
157 |
158 | # @api private
159 | def to_middlewares
160 | middlewares.values.flatten
161 | end
162 |
163 | # @api private
164 | def inject(plugs: {}, middleware_specifications: {})
165 | res_mw_specs = RackSupport::MiddlewareSpecification.inject(
166 | self.middleware_specifications, middleware_specifications
167 | )
168 | res_plugs = Plug.inject(
169 | self.plugs, plugs
170 | )
171 | with(
172 | plugs: res_plugs,
173 | middleware_specifications: res_mw_specs
174 | )
175 | end
176 |
177 | # @api private
178 | def call(env)
179 | rack_app.(env)
180 | end
181 |
182 | private
183 |
184 | def app
185 | App.new(operations.values).freeze
186 | end
187 |
188 | def rack_app
189 | RackSupport::AppWithMiddlewares.new(
190 | to_middlewares,
191 | app
192 | ).freeze
193 | end
194 |
195 | def with(plugs: nil, middleware_specifications: nil)
196 | self.class.new(
197 | context: context,
198 | middleware_specifications: middleware_specifications ||
199 | self.middleware_specifications,
200 | plugs: plugs || self.plugs
201 | )
202 | end
203 | end
204 | end
205 |
--------------------------------------------------------------------------------
/lib/web_pipe/plug.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dry/struct"
4 |
5 | module WebPipe
6 | # @api private
7 | class Plug < Dry::Struct
8 | # Raised when the specification for an operation is invalid.
9 | class InvalidPlugError < ArgumentError
10 | def initialize(name)
11 | super(
12 | <<~MSG
13 | Plug with name +#{name}+ can't be resolved. You must provide
14 | something responding to `#call` or `#to_proc`
15 | . If nothing is given, it's expected to be a method
16 | defined in the context object.
17 | MSG
18 | )
19 | end
20 | end
21 | Name = Types::Strict::Symbol.constructor(&:to_sym)
22 |
23 | Spec = Types.Interface(:call) |
24 | Types.Interface(:to_proc) |
25 | Types.Constant(nil) |
26 | Types::Strict::String |
27 | Types::Strict::Symbol
28 |
29 | Injections = Types::Strict::Hash.map(
30 | Name, Spec
31 | ).default(Types::EMPTY_HASH)
32 |
33 | attribute :name, Name
34 |
35 | attribute :spec, Spec
36 |
37 | def with(new_spec)
38 | new(spec: new_spec)
39 | end
40 |
41 | def call(context)
42 | if spec.respond_to?(:to_proc)
43 | spec.to_proc
44 | elsif spec.respond_to?(:call)
45 | spec
46 | elsif spec.nil?
47 | context.method(name)
48 | else
49 | raise InvalidPlugError, name
50 | end
51 | end
52 |
53 | def self.inject(plugs, injections)
54 | plugs.map do |plug|
55 | inject_plug(plug, injections)
56 | end
57 | end
58 |
59 | def self.inject_plug(plug, injections)
60 | name = plug.name
61 | if injections.key?(name)
62 | plug.with(injections[name])
63 | else
64 | plug
65 | end
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/lib/web_pipe/plugs.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module WebPipe
4 | # Namespace for builders of operations on {WebPipe::Conn}.
5 | #
6 | # Plugs are just higher order functions: functions which return functions
7 | # (operations). For this reason, as a convention its interface is also
8 | # `#call`.
9 | module Plugs
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/web_pipe/plugs/config.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module WebPipe
4 | module Plugs
5 | # Adds given pairs to {Conn#config}.
6 | #
7 | # @example
8 | # class App
9 | # include WebPipe
10 | #
11 | # plug :config, WebPipe::Plugs::Config.(foo: :bar)
12 | # end
13 | module Config
14 | def self.call(pairs)
15 | lambda do |conn|
16 | conn.new(
17 | config: conn.config.merge(pairs)
18 | )
19 | end
20 | end
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/web_pipe/plugs/content_type.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module WebPipe
4 | module Plugs
5 | # Sets `Content-Type` response header.
6 | #
7 | # @example
8 | # class App
9 | # include WebPipe
10 | #
11 | # plug :html, WebPipe::Plugs::ContentType.call('text/html')
12 | # end
13 | module ContentType
14 | # Content-Type header
15 | HEADER = "Content-Type"
16 |
17 | def self.call(content_type)
18 | ->(conn) { conn.add_response_header(HEADER, content_type) }
19 | end
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/lib/web_pipe/rack_support/app_with_middlewares.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rack"
4 |
5 | module WebPipe
6 | module RackSupport
7 | # @api private
8 | class AppWithMiddlewares
9 | attr_reader :rack_middlewares, :app, :builder
10 |
11 | def initialize(rack_middlewares, app)
12 | @rack_middlewares = rack_middlewares
13 | @app = app
14 | @builder = build_rack_app(rack_middlewares, app)
15 | end
16 |
17 | def call(env)
18 | builder.(env)
19 | end
20 |
21 | private
22 |
23 | def build_rack_app(rack_middlewares, app)
24 | Rack::Builder.new.tap do |b|
25 | rack_middlewares.each do |middleware|
26 | b.use(middleware.middleware, *middleware.options)
27 | end
28 | b.run(app)
29 | end
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/web_pipe/rack_support/middleware.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dry/struct"
4 |
5 | module WebPipe
6 | module RackSupport
7 | # Wrapper for a rack middleware.
8 | #
9 | # Simple data structure to represent a rack middleware class with
10 | # its initialization options.
11 | class Middleware < Dry::Struct
12 | # Type for a rack middleware class.
13 | MiddlewareClass = Types.Instance(Class)
14 |
15 | # Type for the options to initialize a rack middleware.
16 | Options = Types::Strict::Array
17 |
18 | # @!attribute [r] middleware
19 | # @return [MiddlewareClass[]] Rack middleware
20 | attribute :middleware, MiddlewareClass
21 |
22 | # @!attribute [r] options
23 | # @return [Options[]] Options to initialize the rack middleware
24 | attribute :options, Options
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/web_pipe/rack_support/middleware_specification.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dry/struct"
4 |
5 | module WebPipe
6 | module RackSupport
7 | # @api private
8 | class MiddlewareSpecification < Dry::Struct
9 | Name = Types::Strict::Symbol.constructor(&:to_sym)
10 |
11 | Spec = Types::Strict::Array
12 |
13 | Injections = Types::Strict::Hash.map(
14 | Name, Spec
15 | ).default(Types::EMPTY_HASH)
16 |
17 | attribute :name, Name
18 |
19 | attribute :spec, Spec
20 |
21 | def self.inject(middleware_specifications, injections)
22 | middleware_specifications.map do |middleware_spec|
23 | inject_middleware(middleware_spec, injections)
24 | end
25 | end
26 |
27 | def self.inject_middleware(middleware_spec, injections)
28 | name = middleware_spec.name
29 | if injections.key?(name)
30 | middleware_spec.with(injections[name])
31 | else
32 | middleware_spec
33 | end
34 | end
35 |
36 | def call
37 | klass_or_pipe = spec[0]
38 | options = spec[1..] || Types::EMPTY_ARRAY
39 | if klass_or_pipe.respond_to?(:to_middlewares)
40 | klass_or_pipe.to_middlewares
41 | elsif klass_or_pipe.is_a?(Class)
42 | [Middleware.new(middleware: klass_or_pipe, options: options)]
43 | end
44 | end
45 |
46 | def with(new_spec)
47 | new(spec: new_spec)
48 | end
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/lib/web_pipe/test_support.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rack/mock"
4 |
5 | module WebPipe
6 | # Test helper methods.
7 | #
8 | # This module is meant to be included in a test file to provide helper
9 | # methods.
10 | module TestSupport
11 | # Builds a {WebPipe::Conn}
12 | #
13 | # @param uri [String] URI that will be used to populate the request
14 | # attributes
15 | # @param attributes [Hash] Manually set attributes for the
16 | # struct. It overrides what is taken from the `uri` parameter
17 | # @param env_opts [Hash] Options to be added to the `env` from which the
18 | # connection struct is created. See {Rack::MockRequest.env_for}.
19 | # @return [Conn]
20 | def build_conn(uri = "", attributes: {}, env_opts: {})
21 | env = Rack::MockRequest.env_for(uri, env_opts)
22 | ConnSupport::Builder.(env)
23 | .new(attributes)
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/web_pipe/types.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dry/types"
4 | require "dry/core/constants"
5 |
6 | module WebPipe
7 | # Namespace for generic types.
8 | module Types
9 | include Dry.Types()
10 | include Dry::Core::Constants
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/web_pipe/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module WebPipe
4 | VERSION = "0.16.0"
5 | end
6 |
--------------------------------------------------------------------------------
/spec/dsl/composition_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 | require "support/middlewares"
6 |
7 | RSpec.describe "Composition" do
8 | let(:pipe) do
9 | Class.new do
10 | # rubocop:disable Lint/ConstantDefinitionInBlock
11 | class App
12 | include WebPipe
13 |
14 | use :first_name, FirstNameMiddleware
15 |
16 | plug :gretting, ->(conn) { conn.add(:greeting, "Hello") }
17 | end
18 | # rubocop:enable Lint/ConstantDefinitionInBlock
19 |
20 | include WebPipe
21 |
22 | compose :app, App.new
23 |
24 | use :last_name, LastNameMiddleware, name: "Doe"
25 | plug :perform_greeting
26 |
27 | private
28 |
29 | def perform_greeting(conn)
30 | first_name = conn.env["first_name"]
31 | last_name = conn.env["last_name"]
32 | greeting = conn.fetch(:greeting)
33 | conn
34 | .set_response_body(
35 | "#{greeting} #{first_name} #{last_name}"
36 | )
37 | end
38 | end.new
39 | end
40 |
41 | it "using a WebPipe composes its middlewares and plugs" do
42 | expect(pipe.(default_env).last[0]).to eq("Hello Joe Doe")
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/spec/dsl/dry_auto_inject_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 | require "dry/core"
6 | require "dry/auto_inject"
7 |
8 | RSpec.describe "Compatibility with dry-auto_inject" do
9 | context "when including web_pipe before dry-auto_inject" do
10 | let(:pipe_class) do
11 | Class.new do
12 | dependency = Class.new do
13 | def message
14 | "From dependency"
15 | end
16 | end
17 |
18 | container = Class.new do
19 | extend Dry::Core::Container::Mixin
20 |
21 | register :dependency do
22 | dependency.new
23 | end
24 | end
25 |
26 | import = Dry::AutoInject(container)
27 |
28 | include WebPipe
29 | include import[:dependency]
30 |
31 | plug :say_hello
32 |
33 | private
34 |
35 | def say_hello(conn)
36 | conn.set_response_body(dependency.message)
37 | end
38 | end
39 | end
40 |
41 | it "uses automatically injected dependencies" do
42 | expect(
43 | pipe_class.new.(default_env).last
44 | ).to eq(["From dependency"])
45 | end
46 |
47 | it "can inject a dependency" do
48 | dependency = Struct.new(:message).new("From injected")
49 |
50 | expect(
51 | pipe_class.new(dependency: dependency).(default_env).last
52 | ).to eq(["From injected"])
53 | end
54 |
55 | it "can inject a dependency and a plug" do
56 | dependency = Struct.new(:message).new("From injected")
57 |
58 | say_hello = ->(conn) { conn.set_response_body("#{dependency.message}, with plug injected") }
59 |
60 | expect(
61 | pipe_class.new(dependency: dependency, plugs: { say_hello: say_hello }).(default_env).last
62 | ).to eq(["From injected, with plug injected"])
63 | end
64 | end
65 |
66 | context "when including web_pipe after dry-auto_inject" do
67 | let(:pipe_class) do
68 | Class.new do
69 | dependency = Class.new do
70 | def message
71 | "From dependency"
72 | end
73 | end
74 |
75 | container = Class.new do
76 | extend Dry::Core::Container::Mixin
77 |
78 | register :dependency do
79 | dependency.new
80 | end
81 | end
82 |
83 | import = Dry::AutoInject(container)
84 |
85 | include import[:dependency]
86 | include WebPipe
87 |
88 | plug :say_hello
89 |
90 | private
91 |
92 | def say_hello(conn)
93 | conn.set_response_body(dependency.message)
94 | end
95 | end
96 | end
97 |
98 | it "uses automatically injected dependencies" do
99 | expect(
100 | pipe_class.new.(default_env).last
101 | ).to eq(["From dependency"])
102 | end
103 |
104 | it "can inject a dependency" do
105 | dependency = Struct.new(:message).new("From injected")
106 |
107 | expect(
108 | pipe_class.new(dependency: dependency).(default_env).last
109 | ).to eq(["From injected"])
110 | end
111 |
112 | it "can inject a dependency and a plug" do
113 | dependency = Struct.new(:message).new("From injected")
114 |
115 | say_hello = ->(conn) { conn.set_response_body("#{dependency.message}, with plug injected") }
116 |
117 | expect(
118 | pipe_class.new(dependency: dependency, plugs: { say_hello: say_hello }).(default_env).last
119 | ).to eq(["From injected, with plug injected"])
120 | end
121 | end
122 | end
123 |
--------------------------------------------------------------------------------
/spec/dsl/inspecting_middlewares_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 | require "support/middlewares"
6 |
7 | RSpec.describe "Inspecting middlewares" do
8 | let(:pipe_class) do
9 | Class.new do
10 | include WebPipe
11 |
12 | use :first_name, FirstNameMiddleware
13 |
14 | plug :hello do |conn|
15 | conn.set_response_body("Hello")
16 | end
17 | end
18 | end
19 |
20 | it "can inspect resolved middlewares" do
21 | pipe = pipe_class.new
22 |
23 | expect(pipe.middlewares[:first_name][0].middleware).to be(FirstNameMiddleware)
24 | end
25 |
26 | it "can inspect injected middlewares" do
27 | last_name = [LastNameMiddleware, { name: "Smith" }]
28 | pipe = pipe_class.new(middlewares: { first_name: last_name })
29 |
30 | expect(pipe.middlewares[:first_name][0].middleware).to be(LastNameMiddleware)
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/spec/dsl/inspecting_operations_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 |
6 | RSpec.describe "Inspecting operations" do
7 | let(:pipe_class) do
8 | Class.new do
9 | include WebPipe
10 |
11 | plug :one, ->(conn) { conn.set_response_body("One") }
12 | end
13 | end
14 |
15 | it "can inspect resolved operations" do
16 | pipe = pipe_class.new
17 | conn = build_conn(default_env)
18 |
19 | expect(
20 | pipe.operations[:one].(conn).response_body
21 | ).to eq(["One"])
22 | end
23 |
24 | it "can inspect injected operations" do
25 | two = ->(conn) { conn.set_response_body("Two") }
26 | pipe = pipe_class.new(plugs: { one: two })
27 | conn = build_conn(default_env)
28 |
29 | expect(
30 | pipe.operations[:one].(conn).response_body
31 | ).to eq(["Two"])
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/spec/dsl/middleware_composition_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 | require "support/middlewares"
6 |
7 | RSpec.describe "Middleware composition" do
8 | let(:pipe) do
9 | Class.new do
10 | # rubocop:disable Lint/ConstantDefinitionInBlock
11 | class AppWithMiddlewares
12 | include WebPipe
13 |
14 | use :first_name, FirstNameMiddleware
15 | use :last_name, LastNameMiddleware, name: "Doe"
16 | end
17 | # rubocop:enable Lint/ConstantDefinitionInBlock
18 |
19 | include WebPipe
20 |
21 | use :app, AppWithMiddlewares.new
22 |
23 | plug :hello
24 |
25 | private
26 |
27 | def hello(conn)
28 | first_name = conn.env["first_name"]
29 | last_name = conn.env["last_name"]
30 | conn
31 | .set_response_body(
32 | "Hello #{first_name} #{last_name}"
33 | )
34 | end
35 | end.new
36 | end
37 |
38 | it "using a WebPipe composes its middlewares" do
39 | expect(pipe.(default_env).last[0]).to eq("Hello Joe Doe")
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/spec/dsl/middleware_injection_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 | require "support/middlewares"
6 |
7 | RSpec.describe "Injecting middlewares" do
8 | let(:pipe) do
9 | Class.new do
10 | include WebPipe
11 |
12 | use :last_name, LastNameMiddleware, name: "Doe"
13 |
14 | plug :hello
15 |
16 | private
17 |
18 | def hello(conn)
19 | last_name = conn.env["last_name"]
20 | conn
21 | .set_response_body(
22 | "Hello Mr./Ms. #{last_name}"
23 | )
24 | end
25 | end.new(middlewares: { last_name: [LastNameMiddleware, { name: "Smith" }] })
26 | end
27 |
28 | it "can use middlewares" do
29 | expect(pipe.(default_env).last[0]).to eq("Hello Mr./Ms. Smith")
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/spec/dsl/middleware_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 | require "support/middlewares"
6 |
7 | RSpec.describe "Using rack middlewares" do
8 | let(:pipe) do
9 | Class.new do
10 | include WebPipe
11 |
12 | use :first_name, FirstNameMiddleware
13 | use :last_name, LastNameMiddleware, name: "Doe"
14 |
15 | plug :hello
16 |
17 | private
18 |
19 | def hello(conn)
20 | first_name = conn.env["first_name"]
21 | last_name = conn.env["last_name"]
22 | conn
23 | .set_response_body(
24 | "Hello #{first_name} #{last_name}"
25 | )
26 | end
27 | end.new
28 | end
29 |
30 | it "can use middlewares" do
31 | expect(pipe.(default_env).last[0]).to eq("Hello Joe Doe")
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/spec/dsl/overriding_instance_methods_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 |
6 | RSpec.describe "Overriding instance methods" do
7 | it "can define custom initialize and call super" do
8 | pipe = Class.new do
9 | include WebPipe
10 |
11 | attr_reader :greeting
12 |
13 | def initialize(greeting:, **kwargs)
14 | @greeting = greeting
15 | super(**kwargs)
16 | end
17 |
18 | plug :name
19 |
20 | plug :render
21 |
22 | private
23 |
24 | def name
25 | raise NotImplementedError
26 | end
27 |
28 | def render(conn)
29 | conn.set_response_body(greeting + conn.fetch(:name))
30 | end
31 | end.new(greeting: "Hello, ", plugs: { name: ->(conn) { conn.add(:name, "Alice") } })
32 |
33 | expect(pipe.(default_env).last[0]).to eq("Hello, Alice")
34 | end
35 |
36 | it "can define custom pipe methods and call super" do
37 | pipe = Class.new do
38 | include WebPipe
39 |
40 | plug :render
41 |
42 | def call(env)
43 | env["body"] = "Hello, world!"
44 | super(env)
45 | end
46 |
47 | private
48 |
49 | def render(conn)
50 | conn.set_response_body(conn.env["body"])
51 | end
52 | end.new
53 |
54 | expect(pipe.(default_env).last[0]).to eq("Hello, world!")
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/spec/dsl/plug_chaining_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 |
6 | RSpec.describe "Chaining plugs" do
7 | let(:pipe) do
8 | Class.new do
9 | include WebPipe
10 |
11 | plug :one
12 | plug :two
13 |
14 | private
15 |
16 | def one(conn)
17 | conn.set_response_body("One")
18 | end
19 |
20 | def two(conn)
21 | conn.set_response_body(
22 | "#{conn.response_body[0]}Two"
23 | )
24 | end
25 | end.new
26 | end
27 |
28 | it "chains successful plugs" do
29 | expect(pipe.(default_env).last).to eq(["OneTwo"])
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/spec/dsl/plug_composition_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 |
6 | RSpec.describe "Plug composition" do
7 | let(:pipe) do
8 | Class.new do
9 | # rubocop:disable Lint/ConstantDefinitionInBlock
10 | class One
11 | include WebPipe
12 |
13 | plug :one
14 |
15 | private
16 |
17 | def one(conn)
18 | conn.set_response_body("One")
19 | end
20 | end
21 | # rubocop:enable Lint/ConstantDefinitionInBlock
22 |
23 | include WebPipe
24 |
25 | plug :one, One.new
26 | plug :two
27 |
28 | private
29 |
30 | def two(conn)
31 | conn.set_response_body(
32 | "#{conn.response_body[0]}Two"
33 | )
34 | end
35 | end.new
36 | end
37 |
38 | it "plugging a WebPipe composes its plug operations" do
39 | expect(pipe.(default_env).last).to eq(["OneTwo"])
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/spec/dsl/plug_from_block_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 |
6 | RSpec.describe "Resolving plugs from a block" do
7 | let(:pipe) do
8 | Class.new do
9 | include WebPipe
10 |
11 | plug :hello do |conn|
12 | conn.set_response_body("Hello, world!")
13 | end
14 | end.new
15 | end
16 |
17 | it "can resolve operation from a block" do
18 | expect(pipe.(default_env).last).to eq(["Hello, world!"])
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/spec/dsl/plug_from_method_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 |
6 | RSpec.describe "Resolving plugs from a method" do
7 | let(:pipe) do
8 | Class.new do
9 | include WebPipe
10 |
11 | plug :hello
12 |
13 | def hello(conn)
14 | conn.set_response_body("Hello, world!")
15 | end
16 | end.new
17 | end
18 |
19 | it "can resolve operation from an internal method" do
20 | expect(pipe.(default_env).last).to eq(["Hello, world!"])
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/spec/dsl/plug_halting_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 |
6 | RSpec.describe "Plug halting" do
7 | let(:pipe) do
8 | Class.new do
9 | include WebPipe
10 |
11 | plug :halt
12 | plug :ongoing
13 |
14 | private
15 |
16 | def halt(conn)
17 | conn.set_response_body("Halted").halt
18 | end
19 |
20 | def ongoing(conn)
21 | conn.set_response_body("Ongoing")
22 | end
23 | end.new
24 | end
25 |
26 | it "halting plug stops the pipe" do
27 | expect(pipe.(default_env).last).to eq(["Halted"])
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/spec/dsl/plug_injection_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 |
6 | RSpec.describe "Plug injection" do
7 | let(:pipe) do
8 | Class.new do
9 | include WebPipe
10 |
11 | plug :hello, ->(conn) { conn.set_response_body("Hello, world!") }
12 | end
13 | end
14 | let(:hello) { ->(conn) { conn.set_response_body("Hello, injected world!") } }
15 |
16 | it "can inject plug as dependency" do
17 | pipe_with_injection = pipe.new(plugs: { hello: hello })
18 |
19 | expect(
20 | pipe_with_injection.(default_env).last
21 | ).to eq(["Hello, injected world!"])
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/spec/dsl/rack_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "rack/test"
5 |
6 | RSpec.describe "Rack application" do
7 | include Rack::Test::Methods
8 |
9 | let(:app) do
10 | Class.new do
11 | include WebPipe
12 |
13 | plug :hello, lambda { |conn|
14 | conn
15 | .set_response_body("Hello, world!")
16 | .set_status(200)
17 | }
18 | end.new
19 | end
20 |
21 | it "is a rack application" do
22 | get "/"
23 |
24 | expect(last_response.body).to eq("Hello, world!")
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/spec/extensions/container/container_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 |
6 | RSpec.describe WebPipe::Conn do
7 | before do
8 | WebPipe.load_extensions(:container)
9 | end
10 |
11 | describe "#container" do
12 | it "returns config's container key" do
13 | conn = build_conn(default_env)
14 | container = {}.freeze
15 |
16 | new_conn = conn.add_config(:container, container)
17 |
18 | expect(new_conn.container).to be(container)
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/extensions/cookies/cookies_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 |
6 | RSpec.describe WebPipe::Conn do
7 | before { WebPipe.load_extensions(:cookies) }
8 |
9 | let(:conn) { build_conn(default_env) }
10 |
11 | describe "#request_cookies" do
12 | it "returns request cookies" do
13 | env = default_env.merge("HTTP_COOKIE" => "foo=bar")
14 |
15 | conn = build_conn(env)
16 |
17 | expect(conn.request_cookies).to eq("foo" => "bar")
18 | end
19 | end
20 |
21 | describe "#set_cookie" do
22 | it "sets given name/value pair to the Set-Cookie header" do
23 | conn = build_conn(default_env)
24 |
25 | new_conn = conn.set_cookie("foo", "bar")
26 |
27 | expect(new_conn.response_headers["Set-Cookie"]).to eq("foo=bar")
28 | end
29 |
30 | it "adds given options to the cookie value" do
31 | conn = build_conn(default_env)
32 |
33 | new_conn = conn.set_cookie("foo", "bar", path: "/")
34 |
35 | expect(new_conn.response_headers["Set-Cookie"]).to eq("foo=bar; path=/")
36 | end
37 | end
38 |
39 | describe "#delete_cookie" do
40 | it "marks given key/value pair cookie for deletion" do
41 | conn = build_conn(default_env)
42 |
43 | new_conn = conn.delete_cookie("foo")
44 |
45 | expect(new_conn.response_headers["Set-Cookie"]).to include(
46 | "foo=; max-age=0; expires="
47 | )
48 | end
49 |
50 | it "adds given options to the cookie value" do
51 | conn = build_conn(default_env)
52 |
53 | new_conn = conn.delete_cookie("foo", domain: "/")
54 |
55 | expect(new_conn.response_headers["Set-Cookie"]).to include(
56 | "foo=; domain=/; max-age=0; expires="
57 | )
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/spec/extensions/dry_schema/dry_schema_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 |
6 | RSpec.describe WebPipe::Conn do
7 | before { WebPipe.load_extensions(:dry_schema) }
8 |
9 | describe "#sanitized_params" do
10 | it "returns config's sanitized key" do
11 | conn = build_conn(default_env)
12 | sanitized_params = {}.freeze
13 |
14 | new_conn = conn.add_config(:sanitized_params, sanitized_params)
15 |
16 | expect(new_conn.fetch_config(:sanitized_params)).to be(sanitized_params)
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/spec/extensions/dry_schema/plugs/sanitize_params_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 | require "dry/schema"
6 | require "web_pipe/extensions/dry_schema/plugs/sanitize_params"
7 |
8 | RSpec.describe WebPipe::Plugs::SanitizeParams do
9 | describe ".call" do
10 | let(:schema) do
11 | Dry::Schema.Params do
12 | required(:name)
13 | end
14 | end
15 |
16 | context "operation on success" do
17 | it "sets sanitized_params bag's key" do
18 | env = default_env.merge(Rack::QUERY_STRING => "name=Joe")
19 | conn = build_conn(env)
20 | operation = described_class.(schema)
21 |
22 | new_conn = operation.(conn)
23 |
24 | expect(new_conn.sanitized_params).to eq(name: "Joe")
25 | end
26 | end
27 |
28 | context "operation on failure" do
29 | it "uses given handler if it is injected" do
30 | configured_handler = lambda do |conn, _result|
31 | conn
32 | .set_response_body("Something went wrong")
33 | .set_status(500)
34 | .halt
35 | end
36 | injected_handler = lambda do |conn, result|
37 | conn
38 | .set_response_body(result.errors.messages.inspect)
39 | .set_status(500)
40 | .halt
41 | end
42 | conn = build_conn(default_env)
43 | .add_config(:param_sanitization_handler, configured_handler)
44 | operation = described_class.(schema, injected_handler)
45 |
46 | new_conn = operation.(conn)
47 |
48 | expect(new_conn.response_body[0]).to include("is missing")
49 | end
50 |
51 | it "uses configured handler if none is injected" do
52 | configured_handler = lambda do |conn, _result|
53 | conn
54 | .set_response_body("Something went wrong")
55 | .set_status(500)
56 | .halt
57 | end
58 | conn = build_conn(default_env)
59 | .add_config(:param_sanitization_handler, configured_handler)
60 | operation = described_class.(schema)
61 |
62 | new_conn = operation.(conn)
63 |
64 | expect(new_conn).to be_halted
65 | expect(new_conn.response_body[0]).to eq("Something went wrong")
66 | end
67 | end
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/spec/extensions/flash/flash_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 | require "web_pipe/conn_support/errors"
6 | require "rack-flash"
7 |
8 | RSpec.describe WebPipe::Conn do
9 | before do
10 | WebPipe.load_extensions(:flash)
11 | end
12 |
13 | let(:flash) { Rack::Flash::FlashHash.new({}) }
14 |
15 | describe "#flash" do
16 | context "when rack-flash key is found in env" do
17 | it "returns its value" do
18 | env = default_env.merge("x-rack.flash" => flash)
19 | conn = build_conn(env)
20 |
21 | expect(conn.flash).to be(flash)
22 | end
23 | end
24 |
25 | context "when rack-flash key is not found in env" do
26 | it "raises a MissingMiddlewareError" do
27 | conn = build_conn(default_env)
28 |
29 | expect { conn.flash }.to raise_error(WebPipe::ConnSupport::MissingMiddlewareError)
30 | end
31 | end
32 | end
33 |
34 | describe "#add_flash" do
35 | it "sets given key to given value in flash" do
36 | env = default_env.merge("x-rack.flash" => flash)
37 | conn = build_conn(env)
38 |
39 | conn.add_flash(:error, "error")
40 |
41 | expect(flash[:error]).to eq("error")
42 | end
43 | end
44 |
45 | describe "#add_flash_now" do
46 | it "sets given key to given value in flash cache" do
47 | env = default_env.merge("x-rack.flash" => flash)
48 | conn = build_conn(env)
49 |
50 | conn.add_flash_now(:error, "error")
51 |
52 | expect(flash.send(:cache)[:error]).to eq("error")
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/spec/extensions/flash/integration/flash_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 | require "rack/session/cookie"
6 | require "rack-flash"
7 |
8 | RSpec.describe "Using flash" do
9 | before { WebPipe.load_extensions(:flash) }
10 |
11 | let(:pipe) do
12 | Class.new do
13 | include WebPipe
14 |
15 | use :session, Rack::Session::Cookie, secret: "secret"
16 | use :flash, Rack::Flash
17 |
18 | plug :add_to_flash
19 | plug :build_response
20 |
21 | private
22 |
23 | def add_to_flash(conn)
24 | conn
25 | .add_flash(:error, "Error")
26 | .add_flash_now(:now, "now")
27 | end
28 |
29 | def build_response(conn)
30 | conn.set_response_body(
31 | "#{conn.flash[:error]} #{conn.flash[:now]}"
32 | )
33 | end
34 | end.new
35 | end
36 |
37 | it "can adadd and read from flash" do
38 | expect(pipe.(default_env).last[0]).to eq("Error now")
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/spec/extensions/hanami_view/fixtures/template_with_context.html.str:
--------------------------------------------------------------------------------
1 | Hello #{name}
--------------------------------------------------------------------------------
/spec/extensions/hanami_view/fixtures/template_with_input.html.str:
--------------------------------------------------------------------------------
1 | Hello #{name}
--------------------------------------------------------------------------------
/spec/extensions/hanami_view/fixtures/template_without_input.html.str:
--------------------------------------------------------------------------------
1 | Hello world
--------------------------------------------------------------------------------
/spec/extensions/hanami_view/hanami_view_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 | require "hanami/view"
6 | require "hanami/view/context"
7 |
8 | RSpec.describe WebPipe::Conn do
9 | before do
10 | WebPipe.load_extensions(:hanami_view)
11 | end
12 |
13 | describe "#view" do
14 | let(:view_class) do
15 | Class.new(Hanami::View) do
16 | config.paths = [File.join(__dir__, "fixtures")]
17 | end
18 | end
19 |
20 | it "sets Rendered string serialization as response body" do
21 | view = Class.new(view_class) do
22 | config.template = "template_without_input"
23 | end
24 | conn = build_conn(default_env)
25 |
26 | new_conn = conn.view(view.new)
27 |
28 | expect(new_conn.response_body).to eq(["Hello world"])
29 | end
30 |
31 | it "passes kwargs along as view's call arguments" do
32 | view = Class.new(view_class) do
33 | config.template = "template_with_input"
34 |
35 | expose :name
36 | end
37 | conn = build_conn(default_env)
38 |
39 | new_conn = conn.view(view.new, name: "Joe")
40 |
41 | expect(new_conn.response_body).to eq(["Hello Joe"])
42 | end
43 |
44 | it "can resolve view from the container" do
45 | view = Class.new(view_class) do
46 | config.template = "template_without_input"
47 | end
48 | container = { "view" => view.new }.freeze
49 | conn = build_conn(default_env)
50 | .add_config(:container, container)
51 |
52 | new_conn = conn.view("view")
53 |
54 | expect(new_conn.response_body).to eq(["Hello world"])
55 | end
56 |
57 | it "initializes view_context class with the generated view_context_options hash" do
58 | view = Class.new(view_class) do
59 | config.template = "template_with_input"
60 | end
61 | context_class = Class.new(Hanami::View::Context) do
62 | attr_reader :name
63 |
64 | def initialize(name:)
65 | super
66 | @name = name
67 | end
68 | end
69 | conn = build_conn(default_env)
70 | .add(:name, "Joe")
71 | .add_config(:view_context_class, context_class)
72 | .add_config(:view_context_options, ->(c) { { name: c.fetch(:name) } })
73 |
74 | new_conn = conn.view(view.new)
75 |
76 | expect(new_conn.response_body).to eq(["Hello Joe"])
77 | end
78 |
79 | it "respects context given at render time if given" do
80 | view = Class.new(view_class) do
81 | config.template = "template_with_input"
82 | end
83 | context = Class.new(Hanami::View::Context) do
84 | def name
85 | "Alice"
86 | end
87 | end.new
88 | conn = build_conn(default_env)
89 |
90 | new_conn = conn.view(view.new, context: context)
91 |
92 | expect(new_conn.response_body).to eq(["Hello Alice"])
93 | end
94 | end
95 | end
96 |
--------------------------------------------------------------------------------
/spec/extensions/not_found/not_found_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 |
6 | RSpec.describe WebPipe::Conn do
7 | before { WebPipe.load_extensions(:not_found) }
8 |
9 | describe "#not_found" do
10 | it "sets 404 status code" do
11 | conn = build_conn
12 |
13 | expect(conn.not_found.status).to be(404)
14 | end
15 |
16 | it "halts it" do
17 | conn = build_conn
18 |
19 | expect(conn.not_found.halted?).to be(true)
20 | end
21 |
22 | context "when no response body is configured" do
23 | it 'sets "Not found" as response body' do
24 | conn = build_conn
25 |
26 | expect(conn.not_found.response_body).to eq(["Not found"])
27 | end
28 | end
29 |
30 | context "when a step to build the response body is configured" do
31 | it "uses it" do
32 | conn = build_conn.add_config(:not_found_body_step,
33 | ->(c) { c.set_response_body("Nothing here") })
34 |
35 | expect(conn.not_found.response_body).to eq(["Nothing here"])
36 | end
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/spec/extensions/params/params/transf_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "web_pipe/extensions/params/params/transf"
5 |
6 | RSpec.describe WebPipe::Params::Transf do
7 | describe ".id" do
8 | it "returns same value" do
9 | expect(described_class[:id].(1)).to be(1)
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/spec/extensions/params/params_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 |
6 | RSpec.describe WebPipe::Conn do
7 | before { WebPipe.load_extensions(:params) }
8 |
9 | describe "#params" do
10 | context "without transformations" do
11 | it "returns request params" do
12 | env = default_env.merge(
13 | Rack::QUERY_STRING => "foo=bar"
14 | )
15 | conn = build_conn(env)
16 |
17 | expect(conn.params).to eq("foo" => "bar")
18 | end
19 | end
20 |
21 | context "with transformations" do
22 | it "uses configured transformations" do
23 | env = default_env.merge(
24 | Rack::QUERY_STRING => "foo=bar&zoo=zoo"
25 | )
26 | conn = build_conn(env)
27 | .add_config(
28 | :param_transformations, [:symbolize_keys, [:reject_keys, [:zoo]]]
29 | )
30 |
31 | expect(conn.params).to eq(foo: "bar")
32 | end
33 |
34 | it "uses injected transformation over configured" do
35 | env = default_env.merge(
36 | Rack::QUERY_STRING => "foo=bar"
37 | )
38 | conn = build_conn(env)
39 | .add_config(
40 | :param_transformations, [:symbolize_keys]
41 | )
42 |
43 | expect(conn.params([:id])).to eq("foo" => "bar")
44 | end
45 |
46 | it "accepts transformations with no extra arguments" do
47 | env = default_env.merge(
48 | Rack::QUERY_STRING => "foo=bar"
49 | )
50 | conn = build_conn(env)
51 | transformations = [:symbolize_keys]
52 |
53 | expect(conn.params(transformations)).to eq(foo: "bar")
54 | end
55 |
56 | it "accepts transformations with extra arguments" do
57 | env = default_env.merge(
58 | Rack::QUERY_STRING => "foo=bar&zoo=zoo"
59 | )
60 | conn = build_conn(env)
61 | transformations = [[:reject_keys, ["zoo"]]]
62 |
63 | expect(conn.params(transformations)).to eq("foo" => "bar")
64 | end
65 |
66 | it "accepts transformations involving the conn instance" do
67 | WebPipe::Params::Transf.register(:from_bar, ->(value, conn) { value.merge(conn.env["bar"]) })
68 | env = default_env.merge(
69 | Rack::QUERY_STRING => "foo=bar",
70 | "bar" => { "bar" => "foo" }
71 | )
72 | conn = build_conn(env)
73 | transformations = [:from_bar]
74 |
75 | expect(conn.params(transformations)).to eq("foo" => "bar", "bar" => "foo")
76 | end
77 |
78 | it "accepts inline transformations" do
79 | conn = build_conn(default_env)
80 | transformations = [->(_params) { { boo: :foo } }]
81 |
82 | expect(conn.params(transformations)).to eq(boo: :foo)
83 | end
84 | end
85 | end
86 | end
87 |
--------------------------------------------------------------------------------
/spec/extensions/rails/rails_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 |
6 | RSpec.describe WebPipe::Conn do
7 | before do
8 | WebPipe.load_extensions(:rails)
9 | end
10 |
11 | # rubocop:disable Lint/ConstantDefinitionInBlock
12 | module ActionController
13 | module Base
14 | def self.renderer
15 | Renderer.new
16 | end
17 |
18 | def self.helpers
19 | {
20 | helper1: "foo"
21 | }
22 | end
23 | end
24 |
25 | class Renderer
26 | def render(*args)
27 | action = args[0]
28 | case action
29 | when "show"
30 | "Show"
31 | else
32 | "Not found"
33 | end
34 | end
35 | end
36 | end
37 | # rubocop:enable Lint/ConstantDefinitionInBlock
38 |
39 | after(:all) { Object.send(:remove_const, :ActionController) }
40 |
41 | describe "#render" do
42 | it "sets rendered as response body" do
43 | conn = build_conn(default_env)
44 |
45 | new_conn = conn.render("show")
46 |
47 | expect(new_conn.response_body).to eq(["Show"])
48 | end
49 |
50 | it "uses configured controller" do
51 | my_controller = Class.new do
52 | def self.renderer
53 | Renderer.new
54 | end
55 |
56 | # rubocop:disable Lint/ConstantDefinitionInBlock
57 | class Renderer
58 | def render(*_args)
59 | "Rendered from MyController"
60 | end
61 | end
62 | # rubocop:enable Lint/ConstantDefinitionInBlock
63 | end
64 | conn = build_conn(default_env)
65 |
66 | new_conn = conn
67 | .add_config(:rails_controller, my_controller)
68 | .render(:whatever)
69 |
70 | expect(new_conn.response_body).to eq(["Rendered from MyController"])
71 | end
72 | end
73 |
74 | describe "#helpers" do
75 | it "returns controller helpers" do
76 | conn = build_conn(default_env)
77 |
78 | expect(conn.helpers).to eq(helper1: "foo")
79 | end
80 |
81 | it "uses configured controller" do
82 | my_controller = Class.new do
83 | def self.helpers
84 | { helper1: "bar" }
85 | end
86 | end
87 | conn = build_conn(default_env)
88 |
89 | new_conn = conn.add_config(:rails_controller, my_controller)
90 |
91 | expect(new_conn.helpers).to eq(helper1: "bar")
92 | end
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/spec/extensions/redirect/redirect_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 |
6 | RSpec.describe WebPipe::Conn do
7 | before { WebPipe.load_extensions(:redirect) }
8 |
9 | describe "#redirect" do
10 | let(:conn) { build_conn(default_env) }
11 | let(:new_conn) { conn.redirect("/here") }
12 |
13 | it "uses 302 as default status code" do
14 | expect(new_conn.status).to be(302)
15 | end
16 |
17 | it "sets given path as Location header" do
18 | expect(new_conn.response_headers["Location"]).to eq("/here")
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/extensions/router_params/router_params_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 |
6 | RSpec.describe WebPipe::Params::Transf do
7 | before { WebPipe.load_extensions(:router_params) }
8 |
9 | describe ".router_params" do
10 | it "merges 'router.params' key from env into given hash" do
11 | env = default_env.merge("router.params" => { foo: :bar })
12 | conn = build_conn(env)
13 |
14 | expect(
15 | described_class[:router_params].with(conn).(bar: :foo)
16 | ).to eq(foo: :bar, bar: :foo)
17 | end
18 |
19 | it "assumes empty hash when 'router.params' is not present" do
20 | conn = build_conn(default_env)
21 |
22 | expect(
23 | described_class[:router_params].with(conn).(bar: :foo)
24 | ).to eq(bar: :foo)
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/spec/extensions/session/session_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 |
6 | RSpec.describe WebPipe::Conn do
7 | before { WebPipe.load_extensions(:session) }
8 |
9 | describe "#session" do
10 | context "when rack session key is found in env" do
11 | it "returns its value" do
12 | session = {}
13 | env = default_env.merge("rack.session" => session)
14 | conn = build_conn(env)
15 |
16 | expect(conn.session).to be(session)
17 | end
18 | end
19 |
20 | context "when rack session key is not found in env" do
21 | it "raises a MissingMiddlewareError" do
22 | conn = build_conn(default_env)
23 |
24 | expect { conn.session }.to raise_error(WebPipe::ConnSupport::MissingMiddlewareError)
25 | end
26 | end
27 | end
28 |
29 | describe "#fetch_session" do
30 | it "returns given item from session" do
31 | env = default_env.merge("rack.session" => { "foo" => "bar" })
32 | conn = build_conn(env)
33 |
34 | expect(conn.fetch_session("foo")).to eq("bar")
35 | end
36 |
37 | it "returns default when not found" do
38 | env = default_env.merge("rack.session" => {})
39 | conn = build_conn(env)
40 |
41 | expect(conn.fetch_session("foo", "bar")).to eq("bar")
42 | end
43 |
44 | it "returns what block returns when key is not found and no default is given" do
45 | env = default_env.merge("rack.session" => {})
46 | conn = build_conn(env)
47 |
48 | expect(conn.fetch_session("foo") { "bar" }).to eq("bar")
49 | end
50 | end
51 |
52 | describe "#add_session" do
53 | it "adds given name/value pair to session" do
54 | env = default_env.merge("rack.session" => {})
55 | conn = build_conn(env)
56 |
57 | new_conn = conn.add_session("foo", "bar")
58 |
59 | expect(new_conn.session["foo"]).to eq("bar")
60 | end
61 | end
62 |
63 | describe "#delete_session" do
64 | it "deletes given name from the session" do
65 | env = default_env.merge("rack.session" => { "foo" => "bar", "bar" => "foo" })
66 | conn = build_conn(env)
67 |
68 | new_conn = conn.delete_session("foo")
69 |
70 | expect(new_conn.session).to eq("bar" => "foo")
71 | end
72 | end
73 |
74 | describe "#clear_session" do
75 | it "resets session" do
76 | env = default_env.merge("rack.session" => { "foo" => "bar" })
77 | conn = build_conn(env)
78 |
79 | new_conn = conn.clear_session
80 |
81 | expect(new_conn.session).to eq({})
82 | end
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/spec/extensions/url/url_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 |
6 | RSpec.describe WebPipe::Conn do
7 | before { WebPipe.load_extensions(:url) }
8 |
9 | def build(env)
10 | build_conn(env)
11 | end
12 |
13 | describe "#base_url" do
14 | it "returns request base url" do
15 | env = default_env.merge(
16 | Rack::HTTPS => "on",
17 | Rack::HTTP_HOST => "www.host.org:8000"
18 | )
19 |
20 | conn = build(env)
21 |
22 | expect(conn.base_url).to eq("https://www.host.org:8000")
23 | end
24 | end
25 |
26 | describe "#path" do
27 | it "returns request path" do
28 | env = default_env.merge(
29 | Rack::SCRIPT_NAME => "index.rb",
30 | Rack::PATH_INFO => "/foo"
31 | )
32 |
33 | conn = build(env)
34 |
35 | expect(conn.path).to eq("index.rb/foo")
36 | end
37 | end
38 |
39 | describe "#full_path" do
40 | it "returns request fullpath" do
41 | env = default_env.merge(
42 | Rack::PATH_INFO => "/foo",
43 | Rack::QUERY_STRING => "foo=bar"
44 | )
45 |
46 | conn = build(env)
47 |
48 | expect(conn.full_path).to eq("/foo?foo=bar")
49 | end
50 | end
51 |
52 | describe "#url" do
53 | it "returns request url" do
54 | env = default_env.merge(
55 | Rack::HTTPS => "on",
56 | Rack::HTTP_HOST => "www.host.org:8000",
57 | Rack::PATH_INFO => "/home",
58 | Rack::QUERY_STRING => "foo=bar"
59 | )
60 |
61 | conn = build(env)
62 |
63 | expect(conn.url).to eq("https://www.host.org:8000/home?foo=bar")
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "web_pipe"
4 | require "pry-byebug"
5 | require "simplecov"
6 |
7 | unless ENV["NO_COVERAGE"]
8 | SimpleCov.start do
9 | add_filter %r{^/spec/}
10 | enable_coverage :branch
11 | enable_coverage_for_eval
12 | end
13 | end
14 |
15 | # https://github.com/dry-rb/dry-configurable/issues/70
16 | WebPipe.load_extensions(
17 | *WebPipe.instance_variable_get(:@__available_extensions__).keys
18 | )
19 |
20 | RSpec.configure do |config|
21 | # Enable flags like --only-failures and --next-failure
22 | config.example_status_persistence_file_path = ".rspec_status"
23 |
24 | # Disable RSpec exposing methods globally on `Module` and `main`
25 | config.disable_monkey_patching!
26 |
27 | config.expect_with :rspec do |c|
28 | c.syntax = :expect
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/spec/support/conn.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rack"
4 |
5 | # Minimal rack's env.
6 | #
7 | # @return [Hash]
8 | # rubocop:disable Metrics/MethodLength
9 | def default_env
10 | {
11 | Rack::RACK_VERSION => Rack::VERSION,
12 | Rack::RACK_INPUT => StringIO.new,
13 | Rack::RACK_ERRORS => StringIO.new,
14 | Rack::RACK_MULTITHREAD => true,
15 | Rack::RACK_MULTIPROCESS => true,
16 | Rack::RACK_RUNONCE => false,
17 | Rack::RACK_URL_SCHEME => "http",
18 | # PEP333
19 | Rack::REQUEST_METHOD => Rack::GET,
20 | Rack::QUERY_STRING => "",
21 | Rack::SERVER_NAME => "www.example.org",
22 | Rack::SERVER_PORT => "80"
23 | }
24 | end
25 | # rubocop:enable Metrics/MethodLength
26 |
27 | # Helper to build a `Conn` from rack's env.
28 | #
29 | # @param env [Hash]
30 | # @return Conn [WebPipe::Conn]
31 | def build_conn(env = default_env)
32 | WebPipe::ConnSupport::Builder.(env)
33 | end
34 |
--------------------------------------------------------------------------------
/spec/support/middlewares.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class FirstNameMiddleware
4 | attr_reader :app
5 |
6 | def initialize(app)
7 | @app = app
8 | end
9 |
10 | def call(env)
11 | env["first_name"] = "Joe"
12 | app.(env)
13 | end
14 | end
15 |
16 | class LastNameMiddleware
17 | attr_reader :app, :name
18 |
19 | def initialize(app, opts)
20 | @app = app
21 | @name = opts[:name]
22 | end
23 |
24 | def call(env)
25 | env["last_name"] = name
26 | app.(env)
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/spec/unit/web_pipe/app_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 |
6 | RSpec.describe WebPipe::App do
7 | describe "#call" do
8 | it "chains operations on Conn" do
9 | op1 = ->(conn) { conn.set_status(200) }
10 | op2 = ->(conn) { conn.set_response_body("foo") }
11 |
12 | app = described_class.new([op1, op2])
13 |
14 | expect(app.(default_env)).to eq([200, {}, ["foo"]])
15 | end
16 |
17 | it "stops chain propagation once a conn is halted" do
18 | op1 = ->(conn) { conn.set_status(200) }
19 | op2 = ->(conn) { conn.set_response_body("foo") }
20 | op3 = ->(conn) { conn.halt }
21 | op4 = ->(conn) { conn.set_response_body("bar") }
22 |
23 | app = described_class.new([op1, op2, op3, op4])
24 |
25 | expect(app.(default_env)).to eq([200, {}, ["foo"]])
26 | end
27 |
28 | it "raises InvalidOperationReturn when one operation does not return a Conn" do
29 | op = ->(_conn) { :foo }
30 |
31 | app = described_class.new([op])
32 |
33 | expect do
34 | app.(default_env)
35 | end.to raise_error(
36 | WebPipe::ConnSupport::Composition::InvalidOperationResult
37 | )
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/spec/unit/web_pipe/conn_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "web_pipe/conn_support/errors"
4 | require "support/conn"
5 | require "rack"
6 |
7 | RSpec.describe WebPipe::Conn do
8 | def build(env)
9 | build_conn(env)
10 | end
11 |
12 | describe "set_status" do
13 | it "sets status" do
14 | conn = build(default_env)
15 |
16 | new_conn = conn.set_status(404)
17 |
18 | expect(new_conn.status).to be(404)
19 | end
20 | end
21 |
22 | describe "set_response_body" do
23 | context "when value is a string" do
24 | it "sets response body as one item array of given value" do
25 | conn = build(default_env).new(response_body: ["foo"])
26 |
27 | new_conn = conn.set_response_body("bar")
28 |
29 | expect(new_conn.response_body).to eq(["bar"])
30 | end
31 | end
32 |
33 | context "when value responds to :each" do
34 | it "it substitutes whole response_body" do
35 | conn = build(default_env).new(response_body: ["foo"])
36 |
37 | new_conn = conn.set_response_body(%w[bar var])
38 |
39 | expect(new_conn.response_body).to eq(%w[bar var])
40 | end
41 | end
42 | end
43 |
44 | describe "set_response_headers" do
45 | it "sets response headers" do
46 | conn = build(default_env)
47 |
48 | new_conn = conn.set_response_headers("foo" => "bar")
49 |
50 | expect(new_conn.response_headers).to eq("Foo" => "bar")
51 | end
52 | end
53 |
54 | describe "add_response_header" do
55 | it "adds given pair to response headers" do
56 | conn = build(default_env).new(
57 | response_headers: { "Foo" => "Foo" }
58 | )
59 |
60 | new_conn = conn.add_response_header("foo_foo", "Bar")
61 |
62 | expect(
63 | new_conn.response_headers
64 | ).to eq("Foo" => "Foo", "Foo-Foo" => "Bar")
65 | end
66 | end
67 |
68 | describe "delete_response_header" do
69 | it "deletes response header with given key" do
70 | conn = build(default_env).new(
71 | response_headers: { "Foo" => "Bar", "Zoo-Zoo" => "Zar" }
72 | )
73 |
74 | new_conn = conn.delete_response_header("zoo_zoo")
75 |
76 | expect(new_conn.response_headers).to eq("Foo" => "Bar")
77 | end
78 | end
79 |
80 | describe "fetch" do
81 | it "returns item in bag with given key" do
82 | conn = build(default_env).new(bag: { foo: :bar })
83 |
84 | expect(conn.fetch(:foo)).to be(:bar)
85 | end
86 |
87 | it "raises KeyNotFoundInBagError when key does not exist" do
88 | conn = build(default_env)
89 |
90 | expect do
91 | conn.fetch(:foo)
92 | end.to raise_error(WebPipe::ConnSupport::KeyNotFoundInBagError)
93 | end
94 |
95 | it "returns default when it is given and key does not exist" do
96 | conn = build(default_env)
97 |
98 | expect(conn.fetch(:foo, :bar)).to be(:bar)
99 | end
100 | end
101 |
102 | describe "add" do
103 | it "adds key/value pair to bag" do
104 | conn = build(default_env)
105 |
106 | new_conn = conn.add(:foo, :bar)
107 |
108 | expect(new_conn.bag[:foo]).to be(:bar)
109 | end
110 | end
111 |
112 | describe "fetch_config" do
113 | it "returns item in config with given key" do
114 | conn = build(default_env).new(config: { foo: :bar })
115 |
116 | expect(conn.fetch_config(:foo)).to be(:bar)
117 | end
118 |
119 | it "raises KeyNotFoundInConfigError when key does not exist" do
120 | conn = build(default_env)
121 |
122 | expect do
123 | conn.fetch_config(:foo)
124 | end.to raise_error(WebPipe::ConnSupport::KeyNotFoundInConfigError)
125 | end
126 |
127 | it "returns default when it is given and key does not exist" do
128 | conn = build(default_env)
129 |
130 | expect(conn.fetch_config(:foo, :bar)).to be(:bar)
131 | end
132 | end
133 |
134 | describe "add" do
135 | it "adds key/value pair to config" do
136 | conn = build(default_env)
137 |
138 | new_conn = conn.add_config(:foo, :bar)
139 |
140 | expect(new_conn.config[:foo]).to be(:bar)
141 | end
142 | end
143 |
144 | describe "#rack_response" do
145 | let(:env) { default_env.merge(Rack::RACK_SESSION => { "foo" => "bar" }) }
146 | let(:conn) do
147 | conn = build(env)
148 | conn
149 | .add_response_header("Content-Type", "text/plain")
150 | .set_status(404)
151 | .set_response_body("Not found")
152 | end
153 | let(:rack_response) { conn.rack_response }
154 |
155 | it "builds status from status attribute" do
156 | expect(conn.rack_response[0]).to be(404)
157 | end
158 |
159 | it "builds response headers from response_headers attribute" do
160 | expect(conn.rack_response[1]).to eq("Content-Type" => "text/plain")
161 | end
162 |
163 | it "builds response body from response_body attribute" do
164 | expect(conn.rack_response[2]).to eq(["Not found"])
165 | end
166 | end
167 |
168 | describe "#halted?" do
169 | it "returns true when class is a Halted instance" do
170 | conn = build_conn(default_env).halt
171 |
172 | expect(conn.halted?).to be(true)
173 | end
174 |
175 | it "returns false when class is a Ongoing instance" do
176 | conn = build_conn(default_env)
177 |
178 | expect(conn.halted?).to be(false)
179 | end
180 | end
181 | end
182 |
--------------------------------------------------------------------------------
/spec/unit/web_pipe/conn_support/builder_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 |
6 | RSpec.describe WebPipe::ConnSupport::Builder do
7 | describe ".call" do
8 | it "creates a Conn::Ongoing" do
9 | conn = described_class.(default_env)
10 |
11 | expect(conn).to be_an_instance_of(WebPipe::Conn::Ongoing)
12 | end
13 |
14 | context "env" do
15 | it "fills in with rack env" do
16 | env = default_env
17 |
18 | conn = described_class.(env)
19 |
20 | expect(conn.env).to be(env)
21 | end
22 | end
23 |
24 | context "request" do
25 | it "fills in with rack request" do
26 | env = default_env
27 |
28 | conn = described_class.(env)
29 |
30 | expect(conn.request).to be_an_instance_of(Rack::Request)
31 | end
32 | end
33 |
34 | context "scheme" do
35 | it "fills with request scheme as symbol" do
36 | env = default_env.merge(Rack::HTTPS => "on")
37 |
38 | conn = described_class.(env)
39 |
40 | expect(conn.scheme).to eq(:https)
41 | end
42 | end
43 |
44 | context "request_method" do
45 | it "fills in with downcased request method as symbol" do
46 | env = default_env.merge(Rack::REQUEST_METHOD => "POST")
47 |
48 | conn = described_class.(env)
49 |
50 | expect(conn.request_method).to eq(:post)
51 | end
52 | end
53 |
54 | context "host" do
55 | it "fills in with request host" do
56 | env = default_env.merge(Rack::HTTP_HOST => "www.host.org")
57 |
58 | conn = described_class.(env)
59 |
60 | expect(conn.host).to eq("www.host.org")
61 | end
62 | end
63 |
64 | context "ip" do
65 | it "fills in with request ip" do
66 | env = default_env.merge("REMOTE_ADDR" => "0.0.0.0")
67 |
68 | conn = described_class.(env)
69 |
70 | expect(conn.ip).to eq("0.0.0.0")
71 | end
72 |
73 | it "defaults to nil" do
74 | env = default_env
75 |
76 | conn = described_class.(env)
77 |
78 | expect(conn.ip).to be_nil
79 | end
80 | end
81 |
82 | context "port" do
83 | it "fills in with request port" do
84 | env = default_env.merge(Rack::SERVER_PORT => "443")
85 |
86 | conn = described_class.(env)
87 |
88 | expect(conn.port).to eq(443)
89 | end
90 | end
91 |
92 | context "script_name" do
93 | it "fills in with request script name" do
94 | env = default_env.merge(Rack::SCRIPT_NAME => "index.rb")
95 |
96 | conn = described_class.(env)
97 |
98 | expect(conn.script_name).to eq("index.rb")
99 | end
100 | end
101 |
102 | context "path_info" do
103 | it "fills in with request path info" do
104 | env = default_env.merge(Rack::PATH_INFO => "/foo/bar")
105 |
106 | conn = described_class.(env)
107 |
108 | expect(conn.path_info).to eq("/foo/bar")
109 | end
110 | end
111 |
112 | context "query_string" do
113 | it "fills in with request query string" do
114 | env = default_env.merge(Rack::QUERY_STRING => "foo=bar")
115 |
116 | conn = described_class.(env)
117 |
118 | expect(conn.query_string).to eq("foo=bar")
119 | end
120 | end
121 |
122 | context "request_body" do
123 | it "fills in with request body" do
124 | request_body = StringIO.new("foo=bar")
125 | env = default_env.merge(
126 | Rack::RACK_INPUT => request_body
127 | )
128 |
129 | conn = described_class.(env)
130 |
131 | expect(conn.request_body).to eq(request_body)
132 | end
133 | end
134 |
135 | describe "#request_headers" do
136 | it "fills in with extracted headers from env" do
137 | env = default_env.merge("HTTP_FOO_BAR" => "BAR")
138 |
139 | conn = described_class.(env)
140 |
141 | expect(conn.request_headers).to eq("Foo-Bar" => "BAR")
142 | end
143 | end
144 |
145 | context "status" do
146 | it "let it to initialize with its default" do
147 | env = default_env
148 |
149 | conn = described_class.(env)
150 |
151 | expect(conn.status).to be(200)
152 | end
153 | end
154 |
155 | context "response_body" do
156 | it "let it to initialize with its default" do
157 | env = default_env
158 |
159 | conn = described_class.(env)
160 |
161 | expect(conn.response_body).to eq([""])
162 | end
163 | end
164 |
165 | context "response_headers" do
166 | it "let it to initialize with its default" do
167 | env = default_env
168 |
169 | conn = described_class.(env)
170 |
171 | expect(conn.response_headers).to eq({})
172 | end
173 | end
174 | end
175 |
176 | context "bag" do
177 | it "let it to initialize with its default" do
178 | env = default_env
179 |
180 | conn = described_class.(env)
181 |
182 | expect(conn.bag).to eq({})
183 | end
184 | end
185 | end
186 |
--------------------------------------------------------------------------------
/spec/unit/web_pipe/conn_support/composition_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 |
6 | RSpec.describe WebPipe::ConnSupport::Composition do
7 | let(:conn) { build_conn(default_env) }
8 |
9 | describe "#call" do
10 | it "chains operations on Conn" do
11 | op1 = ->(conn) { conn.set_status(200) }
12 | op2 = ->(conn) { conn.set_response_body("foo") }
13 |
14 | app = described_class.new([op1, op2])
15 |
16 | expect(app.(conn)).to eq(
17 | op2.(op1.(conn))
18 | )
19 | end
20 |
21 | it "stops chain propagation once a conn is halted" do
22 | op1 = ->(conn) { conn.set_status(200) }
23 | op2 = ->(conn) { conn.set_response_body("foo") }
24 | op3 = ->(conn) { conn.halt }
25 | op4 = ->(conn) { conn.set_response_body("bar") }
26 |
27 | app = described_class.new([op1, op2, op3, op4])
28 |
29 | expect(app.(conn)).to eq(
30 | op3.(op2.(op1.(conn)))
31 | )
32 | end
33 |
34 | it "raises InvalidOperationReturn when one operation does not return a Conn" do
35 | op = ->(_conn) { :foo }
36 |
37 | app = described_class.new([op])
38 |
39 | expect do
40 | app.(conn)
41 | end.to raise_error(WebPipe::ConnSupport::Composition::InvalidOperationResult)
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/spec/unit/web_pipe/conn_support/headers_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 |
6 | RSpec.describe WebPipe::ConnSupport::Headers do
7 | describe ".extract" do
8 | it "returns hash with env HTTP_ pairs with prefix removed" do
9 | env = default_env.merge("HTTP_F" => "BAR")
10 |
11 | headers = described_class.extract(env)
12 |
13 | expect(headers).to eq("F" => "BAR")
14 | end
15 |
16 | it "normalize keys" do
17 | env = default_env.merge("HTTP_FOO_BAR" => "foobar")
18 |
19 | headers = described_class.extract(env)
20 |
21 | expect(headers).to eq("Foo-Bar" => "foobar")
22 | end
23 |
24 | it "includes content type CGI-like var" do
25 | env = default_env.merge("CONTENT_TYPE" => "text/html")
26 |
27 | headers = described_class.extract(env)
28 |
29 | expect(headers["Content-Type"]).to eq("text/html")
30 | end
31 |
32 | it "includes content length CGI-like var" do
33 | env = default_env.merge("CONTENT_LENGTH" => "10")
34 |
35 | headers = described_class.extract(env)
36 |
37 | expect(headers["Content-Length"]).to eq("10")
38 | end
39 |
40 | it "defaults to empty hash" do
41 | headers = described_class.extract(default_env)
42 |
43 | expect(headers).to eq({})
44 | end
45 | end
46 |
47 | describe "add" do
48 | it "adds given pair to given headers" do
49 | headers = { "Foo" => "Bar" }
50 |
51 | new_headers = described_class.add(headers, "Bar", "Foo")
52 |
53 | expect(new_headers).to eq("Foo" => "Bar", "Bar" => "Foo")
54 | end
55 |
56 | it "normalize key" do
57 | headers = described_class.add({}, "foo_foo", "Bar")
58 |
59 | expect(headers).to eq("Foo-Foo" => "Bar")
60 | end
61 | end
62 |
63 | describe "delete" do
64 | it "deletes response header with given key" do
65 | headers = { "Foo" => "Bar", "Zoo" => "Zar" }
66 |
67 | new_headers = described_class.delete(headers, "Zoo")
68 |
69 | expect(new_headers).to eq("Foo" => "Bar")
70 | end
71 |
72 | it "accepts non normalized key" do
73 | headers = { "Foo-Foo" => "Bar" }
74 |
75 | new_headers = described_class.delete(headers, "foo_foo")
76 |
77 | expect(new_headers).to eq({})
78 | end
79 | end
80 |
81 | describe ".normalize_key" do
82 | it "does PascalCase on - and switches _ by -" do
83 | key = described_class.normalize_key("foo_bar")
84 |
85 | expect(key).to eq("Foo-Bar")
86 | end
87 | end
88 |
89 | describe ".normalize" do
90 | it "returns copy of hash with all keys normalized" do
91 | headers = described_class.normalize("foo_bar" => "zoo")
92 |
93 | expect(headers).to eq("Foo-Bar" => "zoo")
94 | end
95 | end
96 | end
97 |
--------------------------------------------------------------------------------
/spec/unit/web_pipe/pipe_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 | require "support/middlewares"
6 |
7 | RSpec.describe WebPipe::Pipe do
8 | describe "#plug" do
9 | context "when no other plugs are present" do
10 | it "initializes the queue with the given plug" do
11 | pipe = described_class.new.plug(:one, "key")
12 |
13 | expect(pipe.plugs).to eq(
14 | [WebPipe::Plug.new(name: :one, spec: "key")]
15 | )
16 | end
17 | end
18 |
19 | context "when other plugs are present" do
20 | it "adds the new plug at the end of the queue" do
21 | pipe = described_class
22 | .new
23 | .plug(:one)
24 | .plug(:two, "key")
25 |
26 | expect(pipe.plugs).to eq(
27 | [
28 | WebPipe::Plug.new(name: :one, spec: nil),
29 | WebPipe::Plug.new(name: :two, spec: "key")
30 | ]
31 | )
32 | end
33 | end
34 |
35 | it "returns a new instance" do
36 | pipe1 = described_class.new
37 |
38 | pipe2 = pipe1.plug(:one)
39 |
40 | expect(pipe2).to be_an_instance_of(described_class)
41 | expect(pipe2).not_to eq(pipe1)
42 | end
43 | end
44 |
45 | describe "#use" do
46 | context "when no other middleware specifications are present" do
47 | it "initializes the queue with the given specification" do
48 | pipe = described_class.new.use(:one, FirstNameMiddleware)
49 |
50 | expect(pipe.middleware_specifications).to eq(
51 | [WebPipe::RackSupport::MiddlewareSpecification.new(name: :one, spec: [FirstNameMiddleware])]
52 | )
53 | end
54 | end
55 |
56 | context "when other middleware specifications are present" do
57 | it "adds the new specification at the end of the queue" do
58 | pipe = described_class
59 | .new
60 | .use(:one, FirstNameMiddleware)
61 | .use(:two, LastNameMiddleware, name: "Alice")
62 |
63 | expect(pipe.middleware_specifications).to eq(
64 | [
65 | WebPipe::RackSupport::MiddlewareSpecification.new(name: :one, spec: [FirstNameMiddleware]),
66 | WebPipe::RackSupport::MiddlewareSpecification.new(name: :two,
67 | spec: [
68 | LastNameMiddleware, { name: "Alice" }
69 | ])
70 | ]
71 | )
72 | end
73 | end
74 |
75 | it "returns a new instance" do
76 | pipe1 = described_class.new
77 |
78 | pipe2 = pipe1.use(:one, Object)
79 |
80 | expect(pipe2).to be_an_instance_of(described_class)
81 | expect(pipe2).not_to eq(pipe1)
82 | end
83 | end
84 |
85 | describe "#compose" do
86 | let(:base) { described_class.new }
87 |
88 | it "adds the plug to the queue" do
89 | pipe = described_class.new.compose(:one, base)
90 |
91 | expect(pipe.plugs).to eq(
92 | [WebPipe::Plug.new(name: :one, spec: base)]
93 | )
94 | end
95 |
96 | it "adds the middleware specification to the queue" do
97 | pipe = described_class.new.compose(:one, base)
98 |
99 | expect(pipe.middleware_specifications).to eq(
100 | [WebPipe::RackSupport::MiddlewareSpecification.new(name: :one, spec: [base])]
101 | )
102 | end
103 |
104 | it "returns a new instance" do
105 | pipe1 = described_class.new
106 |
107 | pipe2 = pipe1.compose(:one, base)
108 |
109 | expect(pipe2).to be_an_instance_of(described_class)
110 | expect(pipe2).not_to eq(pipe1)
111 | end
112 | end
113 |
114 | describe "#operations" do
115 | it "maps plug names with resolved operations" do
116 | pipe = described_class.new
117 | .plug(:one, proc { |_conn| "one" })
118 | .plug(:two, proc { |_conn| "two" })
119 |
120 | operations = pipe.operations
121 |
122 | expect(operations.map { |name, op| [name, op.()] }).to eq([[:one, "one"], [:two, "two"]])
123 | end
124 | end
125 |
126 | describe "#middlewares" do
127 | it "maps middleware specifications names with resolved middlewares" do
128 | pipe = described_class.new
129 | .use(:one, FirstNameMiddleware)
130 | .use(:two, LastNameMiddleware, name: "Alice")
131 |
132 | middlewares = pipe.middlewares
133 |
134 | expect(middlewares).to eq(
135 | {
136 | one: [WebPipe::RackSupport::Middleware.new(middleware: FirstNameMiddleware, options: [])],
137 | two: [WebPipe::RackSupport::Middleware.new(middleware: LastNameMiddleware, options: [{ name: "Alice" }])]
138 | }
139 | )
140 | end
141 | end
142 |
143 | describe "#to_proc" do
144 | it "returns the kleisli composition of all the plugged operations" do
145 | pipe = described_class.new
146 | .plug(:one, ->(conn) { conn.set_response_body("one") })
147 | .plug(:two, ->(conn) { conn.halt })
148 | .plug(:three, ->(conn) { conn.set_response_body("three") })
149 |
150 | to_proc = pipe.to_proc
151 |
152 | expect(to_proc.(build_conn).response_body).to eq(["one"])
153 | end
154 | end
155 |
156 | describe "#to_middlewares" do
157 | it "returns all used middlewares" do
158 | pipe = described_class.new
159 | .use(:one, FirstNameMiddleware)
160 | .use(:two, LastNameMiddleware, name: "Alice")
161 |
162 | to_middlewares = pipe.to_middlewares
163 |
164 | expect(to_middlewares).to eq(
165 | [
166 | WebPipe::RackSupport::Middleware.new(middleware: FirstNameMiddleware, options: []),
167 | WebPipe::RackSupport::Middleware.new(middleware: LastNameMiddleware, options: [{ name: "Alice" }])
168 | ]
169 | )
170 | end
171 | end
172 |
173 | describe "#inject" do
174 | it "substitutes matching plug operation" do
175 | pipe = described_class.new
176 | .plug(:one, "one")
177 | .plug(:two, "two")
178 | .inject(plugs: { one: "injected" })
179 |
180 | expect(pipe.plugs).to eq(
181 | [
182 | WebPipe::Plug.new(name: :one, spec: "injected"),
183 | WebPipe::Plug.new(name: :two, spec: "two")
184 | ]
185 | )
186 | end
187 |
188 | it "substitutes matching middleware specifications" do
189 | pipe = described_class.new
190 | .use(:one, FirstNameMiddleware)
191 | .use(:two, LastNameMiddleware, name: "Alice")
192 | .inject(middleware_specifications: { two: [FirstNameMiddleware] })
193 |
194 | expect(pipe.middleware_specifications).to eq(
195 | [
196 | WebPipe::RackSupport::MiddlewareSpecification.new(name: :one, spec: [FirstNameMiddleware]),
197 | WebPipe::RackSupport::MiddlewareSpecification.new(name: :two, spec: [FirstNameMiddleware])
198 | ]
199 | )
200 | end
201 | end
202 |
203 | describe "#call" do
204 | it "behaves like a rack application" do
205 | pipe = described_class.new
206 | .plug :hello do |conn|
207 | conn
208 | .set_response_body("Hello, world!")
209 | .set_status(200)
210 | end
211 |
212 | expect(pipe.(default_env)).to eq([200, {}, ["Hello, world!"]])
213 | end
214 |
215 | it "can resolve plug operation from a callable" do
216 | one = ->(conn) { conn.set_response_body("One") }
217 | pipe = described_class.new
218 | .plug(:one, one)
219 |
220 | response = pipe.(default_env)
221 |
222 | expect(response.last).to eq(["One"])
223 | end
224 |
225 | it "can resolve plug operation from a block" do
226 | pipe = described_class.new
227 | .plug :one do |conn|
228 | conn.set_response_body("One")
229 | end
230 |
231 | response = pipe.(default_env)
232 |
233 | expect(response.last).to eq(["One"])
234 | end
235 |
236 | it "can resolve plug operation from the context object" do
237 | context = Class.new do
238 | def self.one(conn)
239 | conn.set_response_body(["One"])
240 | end
241 | end
242 | pipe = described_class.new(context: context)
243 | .plug(:one)
244 |
245 | response = pipe.(default_env)
246 |
247 | expect(response.last).to eq(["One"])
248 | end
249 |
250 | it "can resolve plug operation from something responding to to_proc" do
251 | one = Class.new do
252 | def self.to_proc
253 | ->(conn) { conn.set_response_body("One") }
254 | end
255 | end
256 | pipe = described_class.new
257 | .plug(:one, one)
258 |
259 | response = pipe.(default_env)
260 |
261 | expect(response.last).to eq(["One"])
262 | end
263 |
264 | it "can resolve plug operation from another pipe" do
265 | one = ->(conn) { conn.set_response_body("One") }
266 | two = ->(conn) { conn.set_response_body("#{conn.response_body[0]}Two") }
267 | three = ->(conn) { conn.set_response_body("#{conn.response_body[0]}Three") }
268 | pipe1 = described_class.new
269 | .plug(:two, two)
270 | .plug(:three, three)
271 | pipe2 = described_class.new
272 | .plug(:one, one)
273 | .plug(:pipe1, pipe1)
274 |
275 | response = pipe2.(default_env)
276 |
277 | expect(response.last).to eq(["OneTwoThree"])
278 | end
279 |
280 | it "chains plug operations" do
281 | one = ->(conn) { conn.set_response_body("One") }
282 | two = ->(conn) { conn.set_response_body("#{conn.response_body[0]}Two") }
283 | pipe = described_class.new
284 | .plug(:one, one)
285 | .plug(:two, two)
286 |
287 | response = pipe.(default_env)
288 |
289 | expect(response.last).to eq(["OneTwo"])
290 | end
291 |
292 | it "stops chain of plugs when halting" do
293 | one = ->(conn) { conn.set_response_body("One") }
294 | two = ->(conn) { conn.halt }
295 | three = ->(conn) { conn.set_response_body("Three") }
296 | pipe = described_class.new
297 | .plug(:one, one)
298 | .plug(:two, two)
299 | .plug(:three, three)
300 |
301 | response = pipe.(default_env)
302 |
303 | expect(response.last).to eq(["One"])
304 | end
305 |
306 | it "keeps stoping the chain if halted when plugging another pipe" do
307 | one = ->(conn) { conn.set_response_body("One") }
308 | two = ->(conn) { conn.halt }
309 | three = ->(conn) { conn.set_response_body("Three") }
310 | pipe1 = described_class.new
311 | .plug(:two, two)
312 | .plug(:three, three)
313 | pipe2 = described_class.new
314 | .plug(:one, one)
315 | .plug(:pipe1, pipe1)
316 |
317 | response = pipe2.(default_env)
318 |
319 | expect(response.last).to eq(["One"])
320 | end
321 |
322 | it "can use a middleware" do
323 | hello = lambda do |conn|
324 | first_name = conn.env["first_name"]
325 | last_name = conn.env["last_name"]
326 | conn
327 | .set_response_body(
328 | "Hello #{first_name} #{last_name}"
329 | )
330 | end
331 | pipe = described_class.new
332 | .use(:first_name, FirstNameMiddleware)
333 | .use(:last_name, LastNameMiddleware, name: "Doe")
334 | .plug(:hello, hello)
335 |
336 | response = pipe.(default_env)
337 |
338 | expect(response.last).to eq(["Hello Joe Doe"])
339 | end
340 |
341 | it "can use middlewares from something responding to #to_middlewares" do
342 | middlewares = Class.new do
343 | def self.to_middlewares
344 | [
345 | WebPipe::RackSupport::Middleware.new(middleware: FirstNameMiddleware, options: []),
346 | WebPipe::RackSupport::Middleware.new(middleware: LastNameMiddleware, options: [{ name: "Doe" }])
347 | ]
348 | end
349 | end
350 | hello = lambda do |conn|
351 | first_name = conn.env["first_name"]
352 | last_name = conn.env["last_name"]
353 | conn
354 | .set_response_body(
355 | "Hello #{first_name} #{last_name}"
356 | )
357 | end
358 | pipe = described_class.new
359 | .use(:middlewaers, middlewares)
360 | .plug(:hello, hello)
361 |
362 | response = pipe.(default_env)
363 |
364 | expect(response.last).to eq(["Hello Joe Doe"])
365 | end
366 |
367 | it "can use middlewares from another pipe" do
368 | pipe1 = described_class.new
369 | .use(:first_name, FirstNameMiddleware)
370 | .use(:last_name, LastNameMiddleware, name: "Doe")
371 | hello = lambda do |conn|
372 | first_name = conn.env["first_name"]
373 | last_name = conn.env["last_name"]
374 | conn
375 | .set_response_body(
376 | "Hello #{first_name} #{last_name}"
377 | )
378 | end
379 | pipe = described_class.new
380 | .use(:pipe1, pipe1)
381 | .plug(:hello, hello)
382 |
383 | response = pipe.(default_env)
384 |
385 | expect(response.last).to eq(["Hello Joe Doe"])
386 | end
387 | end
388 | end
389 |
--------------------------------------------------------------------------------
/spec/unit/web_pipe/plug_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | RSpec.describe WebPipe::Plug do
6 | let(:object) do
7 | Class.new do
8 | def public; end
9 |
10 | private
11 |
12 | def private; end
13 | end.new
14 | end
15 |
16 | describe "#with" do
17 | let(:name) { :name }
18 | let(:plug) { described_class.new(name: name, spec: nil) }
19 |
20 | let(:new_spec) { -> {} }
21 | let(:new_plug) { plug.with(new_spec) }
22 |
23 | it "returns new instance" do
24 | expect(new_plug).not_to be(plug)
25 | end
26 |
27 | it "keeps plug name" do
28 | expect(new_plug.name).to be(name)
29 | end
30 |
31 | it "replaces spec" do
32 | expect(new_plug.spec).to eq(new_spec)
33 | end
34 | end
35 |
36 | describe "#call" do
37 | let(:plug) { described_class.new(name: name, spec: spec) }
38 |
39 | context "when spec responds to #to_proc" do
40 | let(:name) { "name" }
41 | let(:spec) do
42 | Class.new do
43 | def to_proc
44 | -> { "hey" }
45 | end
46 | end.new
47 | end
48 |
49 | it "returns the result of calling it" do
50 | expect(plug.(object).()).to eq("hey")
51 | end
52 | end
53 |
54 | context "when spec is callable" do
55 | let(:name) { "name" }
56 | let(:spec) { -> {} }
57 |
58 | it "returns it" do
59 | expect(plug.(object)).to be(spec)
60 | end
61 | end
62 |
63 | context "when spec is nil" do
64 | let(:spec) { nil }
65 |
66 | context "when object has a public method with plug name" do
67 | let(:name) { "public" }
68 |
69 | it "returns a proc wrapping it" do
70 | expect(plug.(object)).to eq(object.method(:public))
71 | end
72 | end
73 |
74 | context "when object has a private method with plug name" do
75 | let(:name) { "private" }
76 |
77 | it "returns a proc wrapping it" do
78 | expect(plug.(object)).to eq(object.method(:private))
79 | end
80 | end
81 | end
82 | end
83 |
84 | describe ".inject" do
85 | it "inject specs" do
86 | plugs = [
87 | described_class.new(name: :op1, spec: -> { "op1" }),
88 | described_class.new(name: :op2, spec: -> { "op2" })
89 | ]
90 | injected = { op2: -> { "injected" } }
91 |
92 | result = described_class.inject(
93 | plugs, injected
94 | )
95 |
96 | expect(result.map(&:name)).to eq(%i[op1 op2])
97 | expect(result.map { |plug| plug.(self) }.map(&:call)).to eq(%w[op1 injected])
98 | end
99 | end
100 | end
101 |
--------------------------------------------------------------------------------
/spec/unit/web_pipe/plugs/config_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 |
6 | RSpec.describe WebPipe::Plugs::Config do
7 | describe ".call" do
8 | it "creates an operation which adds given pairs to config" do
9 | conn = build_conn(default_env).add_config(:zoo, :zoo)
10 | operation = described_class.(foo: :bar,
11 | rar: :ror)
12 |
13 | new_conn = operation.(conn)
14 |
15 | expect(new_conn.config).to eq(zoo: :zoo, foo: :bar, rar: :ror)
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/spec/unit/web_pipe/plugs/content_type_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/conn"
5 |
6 | RSpec.describe WebPipe::Plugs::ContentType do
7 | describe ".call" do
8 | it "creates an operation which adds given argument as Content-Type header" do
9 | conn = build_conn(default_env)
10 | operation = described_class.("text/html")
11 |
12 | new_conn = operation.(conn)
13 |
14 | expect(new_conn.response_headers["Content-Type"]).to eq("text/html")
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/unit/web_pipe/rack/middleware_specification_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "support/middlewares"
5 |
6 | RSpec.describe WebPipe::RackSupport::MiddlewareSpecification do
7 | let(:pipe) do
8 | Class.new do
9 | include WebPipe
10 |
11 | use :middleware, FirstNameMiddleware
12 | end
13 | end
14 |
15 | describe "#call" do
16 | context "when spec responds to to_middlewares" do
17 | it "returns an array with its WebPipe::Rack::Middleware's" do
18 | expect(described_class.new(name: :name, spec: [pipe.new]).()).to include(
19 | WebPipe::RackSupport::Middleware.new(middleware: FirstNameMiddleware, options: [])
20 | )
21 | end
22 | end
23 |
24 | context "when spec is a class" do
25 | it "returns it as a WebPipe::RackSupport::Middleware with empty options" do
26 | expect(described_class.new(name: :name, spec: [FirstNameMiddleware]).()).to eq(
27 | [WebPipe::RackSupport::Middleware.new(middleware: FirstNameMiddleware, options: [])]
28 | )
29 | end
30 | end
31 |
32 | context "when spec is a class with options" do
33 | it "returns it as a WebPipe::RackSupport::Middleware with given options" do
34 | expect(described_class.new(name: :name, spec: [LastNameMiddleware, { name: "Joe" }]).()).to eq(
35 | [WebPipe::RackSupport::Middleware.new(middleware: LastNameMiddleware, options: [name: "Joe"])]
36 | )
37 | end
38 | end
39 | end
40 |
41 | describe "#with" do
42 | let(:name) { :name }
43 | let(:middleware_specification) { described_class.new(name: name, spec: [pipe.new]) }
44 |
45 | let(:new_spec) { [FirstNameMiddleware] }
46 | let(:new_middleware_specification) { middleware_specification.with(new_spec) }
47 |
48 | it "returns new instance" do
49 | expect(new_middleware_specification).not_to be(middleware_specification)
50 | end
51 |
52 | it "keeps plug name" do
53 | expect(new_middleware_specification.name).to be(name)
54 | end
55 |
56 | it "replaces spec" do
57 | expect(new_middleware_specification.spec).to eq(new_spec)
58 | end
59 | end
60 |
61 | describe ".inject" do
62 | it "inject specs" do
63 | spec1 = described_class.new(name: :middleware1, spec: [FirstNameMiddleware])
64 | spec2 = described_class.new(name: :middleware2, spec: [pipe.new])
65 | middleware_specifications = [
66 | spec1,
67 | spec2
68 | ]
69 | injections = { middleware2: [FirstNameMiddleware] }
70 |
71 | result = described_class.inject(
72 | middleware_specifications, injections
73 | )
74 |
75 | expect(result.map(&:spec)).to eq([[FirstNameMiddleware], [FirstNameMiddleware]])
76 | end
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/spec/unit/web_pipe/test_support_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | RSpec.describe WebPipe::TestSupport do
6 | let(:klass) do
7 | Class.new { include WebPipe::TestSupport }.new
8 | end
9 |
10 | describe "#build_conn" do
11 | it "returns a WebPipe::Conn::Ongoing instance" do
12 | expect(
13 | klass.build_conn.instance_of?(WebPipe::Conn::Ongoing)
14 | ).to be(true)
15 | end
16 |
17 | it "can shortcut through uri" do
18 | conn = klass.build_conn("http://dummy.org?foo=bar")
19 |
20 | expect(conn.host).to eq("dummy.org")
21 | expect(conn.query_string).to eq("foo=bar")
22 | end
23 |
24 | it "can override attributes" do
25 | conn = klass.build_conn(attributes: { host: "foo.bar" })
26 |
27 | expect(conn.host).to eq("foo.bar")
28 | end
29 |
30 | it "gives preference to the attributes over the uri" do
31 | conn = klass.build_conn("http://joe.doe", attributes: { host: "foo.bar" })
32 |
33 | expect(conn.host).to eq("foo.bar")
34 | end
35 |
36 | it "can forward env options" do
37 | conn = klass.build_conn(env_opts: { method: "PUT" })
38 |
39 | expect(conn.request_method).to be(:put)
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/spec/web_pipe_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe WebPipe do
4 | it "has a version number" do
5 | expect(WebPipe::VERSION).not_to be nil
6 | end
7 |
8 | it "configures loader" do
9 | expect do
10 | WebPipe.loader.eager_load(force: true)
11 | end.not_to raise_error
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/web_pipe.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | lib = File.expand_path("lib", __dir__)
4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5 | require "web_pipe/version"
6 |
7 | Gem::Specification.new do |spec|
8 | spec.name = "web_pipe"
9 | spec.version = WebPipe::VERSION
10 | spec.authors = ["Marc Busqué"]
11 | spec.email = ["marc@lamarciana.com"]
12 | spec.homepage = "https://github.com/waiting-for-dev/web_pipe"
13 | spec.summary = "Rack application builder through a pipe of operations on an immutable struct."
14 | spec.licenses = ["MIT"]
15 |
16 | spec.metadata = {
17 | "bug_tracker_uri" => "https://github.com/waiting-for-dev/web_pipe/issues",
18 | "changelog_uri" => "https://github.com/waiting-for-dev/web_pipe/blob/main/CHANGELOG.md",
19 | "documentation_uri" => "https://github.com/waiting-for-dev/web_pipe/blob/main/README.md",
20 | "funding_uri" => "https://github.com/sponsors/waiting-for-dev",
21 | "label" => "web_pipe",
22 | "source_code_uri" => "https://github.com/waiting-for-dev/web_pipe",
23 | "rubygems_mfa_required" => "true"
24 | }
25 |
26 | spec.required_ruby_version = ">= 3.0"
27 |
28 | spec.files = Dir.chdir(File.expand_path(__dir__)) do
29 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
30 | end
31 | spec.require_paths = ["lib"]
32 |
33 | spec.add_runtime_dependency "dry-monads", "~> 1.3"
34 | spec.add_runtime_dependency "dry-struct", "~> 1.0"
35 | spec.add_runtime_dependency "dry-types", "~> 1.1"
36 | spec.add_runtime_dependency "rack", "~> 2.0"
37 | spec.add_runtime_dependency "zeitwerk", "~> 2.6"
38 | end
39 |
--------------------------------------------------------------------------------