web_pipe 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
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
@@ -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
@@ -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
@@ -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