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.
@@ -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