web_pipe 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +10 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +9 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +91 -0
- data/README.md +279 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/dry/monads/result/extensions/either.rb +42 -0
- data/lib/web_pipe.rb +16 -0
- data/lib/web_pipe/app.rb +106 -0
- data/lib/web_pipe/conn.rb +397 -0
- data/lib/web_pipe/conn_support/builder.rb +36 -0
- data/lib/web_pipe/conn_support/errors.rb +16 -0
- data/lib/web_pipe/conn_support/headers.rb +97 -0
- data/lib/web_pipe/conn_support/types.rb +50 -0
- data/lib/web_pipe/dsl/builder.rb +38 -0
- data/lib/web_pipe/dsl/class_context.rb +62 -0
- data/lib/web_pipe/dsl/dsl_context.rb +53 -0
- data/lib/web_pipe/dsl/instance_methods.rb +60 -0
- data/lib/web_pipe/plug.rb +103 -0
- data/lib/web_pipe/rack/app_with_middlewares.rb +61 -0
- data/lib/web_pipe/rack/middleware.rb +33 -0
- data/lib/web_pipe/types.rb +31 -0
- data/lib/web_pipe/version.rb +3 -0
- data/web_pipe.gemspec +51 -0
- metadata +244 -0
data/bin/setup
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require "dry/core/extensions"
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Monads
|
5
|
+
# This is currently a PR in dry-monads:
|
6
|
+
#
|
7
|
+
# https://github.com/dry-rb/dry-monads/pull/84
|
8
|
+
class Result
|
9
|
+
extend Dry::Core::Extensions
|
10
|
+
|
11
|
+
register_extension(:either) do
|
12
|
+
class Success
|
13
|
+
# Returns result of applying first function to the internal value.
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# Dry::Monads.Success(1).either(-> x { x + 1 }, -> x { x + 2 }) # => 2
|
17
|
+
#
|
18
|
+
# @param f [#call] Function to apply
|
19
|
+
# @param g [#call] Ignored
|
20
|
+
# @return [Any] Return value of `f`
|
21
|
+
def either(f, _g)
|
22
|
+
f.(success)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Failure
|
27
|
+
# Returns result of applying second function to the internal value.
|
28
|
+
#
|
29
|
+
# @example
|
30
|
+
# Dry::Monads.Failure(1).either(-> x { x + 1 }, -> x { x + 2 }) # => 3
|
31
|
+
#
|
32
|
+
# @param f [#call] Ignored
|
33
|
+
# @param g [#call] Function to call
|
34
|
+
# @return [Any] Return value of `g`
|
35
|
+
def either(_f, g)
|
36
|
+
g.(failure)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/web_pipe.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'web_pipe/dsl/builder'
|
2
|
+
|
3
|
+
# See [the
|
4
|
+
# README](https://github.com/waiting-for-dev/web_pipe/blob/master/README.md)
|
5
|
+
# for a general overview of this library.
|
6
|
+
module WebPipe
|
7
|
+
# Including just delegates to an instance of `Builder`, so
|
8
|
+
# `Builder#included` is finally called.
|
9
|
+
def self.included(klass)
|
10
|
+
klass.include(call())
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.call(*args)
|
14
|
+
DSL::Builder.new(*args)
|
15
|
+
end
|
16
|
+
end
|
data/lib/web_pipe/app.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'dry/initializer'
|
2
|
+
require 'dry/monads/result'
|
3
|
+
require 'web_pipe/types'
|
4
|
+
require 'web_pipe/conn'
|
5
|
+
require 'web_pipe/conn_support/builder'
|
6
|
+
require 'dry/monads/result/extensions/either'
|
7
|
+
|
8
|
+
Dry::Monads::Result.load_extensions(:either)
|
9
|
+
|
10
|
+
module WebPipe
|
11
|
+
# Rack application built around applying a pipe of {Operation} to
|
12
|
+
# a {Conn}.
|
13
|
+
#
|
14
|
+
# A rack application is something callable accepting rack's `env`
|
15
|
+
# as argument and returning a rack response. So, the workflow
|
16
|
+
# followed to build it is:
|
17
|
+
#
|
18
|
+
# - Take rack's `env` and create a {Conn} from here.
|
19
|
+
# - Starting from it, apply the pipe of operations (anything
|
20
|
+
# callable accepting a {Conn} and returning a {Conn}).
|
21
|
+
# - Convert last {Conn} back to a rack response and
|
22
|
+
# return it.
|
23
|
+
#
|
24
|
+
# {Conn} can itself be of two different types (subclasses of it}:
|
25
|
+
# {Conn::Clean} and {Conn::Dirty}. The pipe is stopped
|
26
|
+
# whenever the stack is emptied or a {Conn::Dirty} is
|
27
|
+
# returned in any of the steps.
|
28
|
+
class App
|
29
|
+
# Type for an operation.
|
30
|
+
#
|
31
|
+
# It should be anything callable expecting a {Conn} and
|
32
|
+
# returning a {Conn}.
|
33
|
+
Operation = Types.Contract(:call)
|
34
|
+
|
35
|
+
# Type for a rack environment.
|
36
|
+
RackEnv = Types::Strict::Hash
|
37
|
+
|
38
|
+
# Error raised when an {Operation} returns something that is not a
|
39
|
+
# {Conn}.
|
40
|
+
class InvalidOperationResult < RuntimeError
|
41
|
+
# @param returned [Any] What was returned from the {Operation}
|
42
|
+
def initialize(returned)
|
43
|
+
super(
|
44
|
+
<<~eos
|
45
|
+
An operation returned +#{returned.inspect}+. To be valid,
|
46
|
+
an operation must return whether a
|
47
|
+
WebPipe::Conn::Clean or a WebPipe::Conn::Dirty.
|
48
|
+
eos
|
49
|
+
)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
include Dry::Monads::Result::Mixin
|
54
|
+
|
55
|
+
include Dry::Initializer.define -> do
|
56
|
+
# @!attribute [r] operations
|
57
|
+
# @return [Array<Operation[]>]
|
58
|
+
param :operations, type: Types.Array(Operation)
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param env [Hash] Rack env
|
62
|
+
#
|
63
|
+
# @return env [Array] Rack response
|
64
|
+
def call(env)
|
65
|
+
extract_rack_response(
|
66
|
+
apply_operations(
|
67
|
+
conn_from_env(
|
68
|
+
RackEnv[env]
|
69
|
+
)
|
70
|
+
)
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def conn_from_env(env)
|
77
|
+
Success(
|
78
|
+
ConnSupport::Builder.(env)
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
def apply_operations(conn)
|
83
|
+
operations.reduce(conn) do |new_conn, operation|
|
84
|
+
new_conn.bind { |c| apply_operation(c, operation) }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def apply_operation(conn, operation)
|
89
|
+
result = operation.(conn)
|
90
|
+
case result
|
91
|
+
when Conn::Clean
|
92
|
+
Success(result)
|
93
|
+
when Conn::Dirty
|
94
|
+
Failure(result)
|
95
|
+
else
|
96
|
+
raise InvalidOperationResult.new(result)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def extract_rack_response(conn)
|
101
|
+
extract_proc = :rack_response.to_proc
|
102
|
+
|
103
|
+
conn.either(extract_proc, extract_proc)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,397 @@
|
|
1
|
+
require 'dry/struct'
|
2
|
+
require 'web_pipe/conn_support/types'
|
3
|
+
require 'web_pipe/conn_support/errors'
|
4
|
+
require 'web_pipe/conn_support/headers'
|
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::Clean} and {Conn::Dirty}. {ConnSupport::Builder} constructs
|
20
|
+
# a {Conn::Clean} struct, while {#taint} copies the data to a
|
21
|
+
# {Conn::Dirty} instance. The intention of this is to halt
|
22
|
+
# operations on the web request/response cycle one a {Conn::Dirty}
|
23
|
+
# instance is detected.
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
# WebPipe::Conn::Builder.call(env).
|
27
|
+
# set_status(404).
|
28
|
+
# add_response_header('Content-Type', 'text/plain').
|
29
|
+
# set_response_body('Not found').
|
30
|
+
# taint
|
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
|
+
# ['<html></html>']
|
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
|
+
# Base part of the URL.
|
213
|
+
#
|
214
|
+
# This is {#scheme} and {#host}, adding {#port} unless it is the
|
215
|
+
# default one for the scheme.
|
216
|
+
#
|
217
|
+
# @return [BaseUrl]
|
218
|
+
#
|
219
|
+
# @example
|
220
|
+
# 'https://example.org'
|
221
|
+
# 'http://example.org:8000'
|
222
|
+
def base_url
|
223
|
+
request.base_url
|
224
|
+
end
|
225
|
+
|
226
|
+
# URL path.
|
227
|
+
#
|
228
|
+
# This is {#script_name} and {#path_info}.
|
229
|
+
#
|
230
|
+
# @return [Path]
|
231
|
+
#
|
232
|
+
# @example
|
233
|
+
# 'index.rb/users'
|
234
|
+
def path
|
235
|
+
request.path
|
236
|
+
end
|
237
|
+
|
238
|
+
# URL full path.
|
239
|
+
#
|
240
|
+
# This is {#path} with {#query_string} if present.
|
241
|
+
#
|
242
|
+
# @return [FullPath]
|
243
|
+
#
|
244
|
+
# @example
|
245
|
+
# '/users?id=1'
|
246
|
+
def full_path
|
247
|
+
request.fullpath
|
248
|
+
end
|
249
|
+
|
250
|
+
# Request URL.
|
251
|
+
#
|
252
|
+
# This is the same as {#base_url} plus {#full_path}.
|
253
|
+
#
|
254
|
+
# @return [Url]
|
255
|
+
#
|
256
|
+
# @example
|
257
|
+
# 'http://www.example.org:8000/users?id=1'
|
258
|
+
def url
|
259
|
+
request.url
|
260
|
+
end
|
261
|
+
|
262
|
+
# GET and POST params merged in a hash.
|
263
|
+
#
|
264
|
+
# @return [Params]
|
265
|
+
#
|
266
|
+
# @example
|
267
|
+
# { 'id' => 1, 'name' => 'Joe' }
|
268
|
+
def params
|
269
|
+
request.params
|
270
|
+
end
|
271
|
+
|
272
|
+
# Sets response status code.
|
273
|
+
#
|
274
|
+
# @param code [StatusCode]
|
275
|
+
#
|
276
|
+
# @return {Conn}
|
277
|
+
def set_status(code)
|
278
|
+
new(
|
279
|
+
status: code
|
280
|
+
)
|
281
|
+
end
|
282
|
+
|
283
|
+
# Sets response body.
|
284
|
+
#
|
285
|
+
# As per rack specification, the response body must respond to
|
286
|
+
# `#each`. Here, when given `content` responds to `:each` it is
|
287
|
+
# set as it is as the new response body. Otherwise, what is set
|
288
|
+
# is a one item array of it.
|
289
|
+
#
|
290
|
+
# @param content [#each, String]
|
291
|
+
#
|
292
|
+
# @return {Conn}
|
293
|
+
#
|
294
|
+
# @see https://www.rubydoc.info/github/rack/rack/master/file/SPEC#label-The+Body
|
295
|
+
def set_response_body(content)
|
296
|
+
new(
|
297
|
+
response_body: content.respond_to?(:each) ? content : [content]
|
298
|
+
)
|
299
|
+
end
|
300
|
+
|
301
|
+
# Adds given pair to response headers.
|
302
|
+
#
|
303
|
+
# `key` is normalized.
|
304
|
+
#
|
305
|
+
# @param key [String]
|
306
|
+
# @param value [String]
|
307
|
+
#
|
308
|
+
# @return {Conn}
|
309
|
+
#
|
310
|
+
# @see ConnSupport::Headers.normalize_key
|
311
|
+
def add_response_header(key, value)
|
312
|
+
new(
|
313
|
+
response_headers: ConnSupport::Headers.add(
|
314
|
+
response_headers, key, value
|
315
|
+
)
|
316
|
+
)
|
317
|
+
end
|
318
|
+
|
319
|
+
# Deletes pair with given key from response headers.
|
320
|
+
#
|
321
|
+
# It accepts a non normalized key.
|
322
|
+
#
|
323
|
+
# @param key [String]
|
324
|
+
#
|
325
|
+
# @return {Conn}
|
326
|
+
#
|
327
|
+
# @see ConnSupport::Headers.normalize_key
|
328
|
+
def delete_response_header(key)
|
329
|
+
new(
|
330
|
+
response_headers: ConnSupport::Headers.delete(
|
331
|
+
response_headers, key
|
332
|
+
)
|
333
|
+
)
|
334
|
+
end
|
335
|
+
|
336
|
+
# Reads an item from {#bag}.
|
337
|
+
#
|
338
|
+
# @param key [Symbol]
|
339
|
+
#
|
340
|
+
# @return [Object]
|
341
|
+
#
|
342
|
+
# @raise ConnSupport::KeyNotFoundInBagError when key is not
|
343
|
+
# registered in the bag.
|
344
|
+
def fetch(key)
|
345
|
+
bag.fetch(key) { raise ConnSupport::KeyNotFoundInBagError.new(key) }
|
346
|
+
end
|
347
|
+
|
348
|
+
# Writes an item to the {#bag}.
|
349
|
+
#
|
350
|
+
# If it already exists, it is overwritten.
|
351
|
+
#
|
352
|
+
# @param key [Symbol]
|
353
|
+
# @param value [Object]
|
354
|
+
#
|
355
|
+
# @return [Conn]
|
356
|
+
def put(key, value)
|
357
|
+
new(
|
358
|
+
bag: bag.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<StatusCode, Headers, ResponseBody>]
|
371
|
+
#
|
372
|
+
# @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 {Dirty} instance and
|
382
|
+
# returns it.
|
383
|
+
#
|
384
|
+
# @return [Dirty]
|
385
|
+
def taint
|
386
|
+
Dirty.new(attributes)
|
387
|
+
end
|
388
|
+
|
389
|
+
# Type of {Conn} representing an ongoing request/response
|
390
|
+
# cycle.
|
391
|
+
class Clean < Conn; end
|
392
|
+
|
393
|
+
# Type of {Conn} representing a halted request/response
|
394
|
+
# cycle.
|
395
|
+
class Dirty < Conn; end
|
396
|
+
end
|
397
|
+
end
|