web_pipe 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|