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
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'web_pipe/conn'
|
3
|
+
require 'web_pipe/conn_support/headers'
|
4
|
+
|
5
|
+
module WebPipe
|
6
|
+
module ConnSupport
|
7
|
+
# Helper module to build a {Conn} from a rack's env.
|
8
|
+
#
|
9
|
+
# It always return a {Conn::Clean} subclass.
|
10
|
+
#
|
11
|
+
# @private
|
12
|
+
module Builder
|
13
|
+
# @param env [Types::Env] Rack's env
|
14
|
+
#
|
15
|
+
# @return [Conn::Clean]
|
16
|
+
def self.call(env)
|
17
|
+
rr = ::Rack::Request.new(env)
|
18
|
+
Conn::Clean.new(
|
19
|
+
request: rr,
|
20
|
+
env: env,
|
21
|
+
|
22
|
+
scheme: rr.scheme.to_sym,
|
23
|
+
request_method: rr.request_method.downcase.to_sym,
|
24
|
+
host: rr.host,
|
25
|
+
ip: rr.ip,
|
26
|
+
port: rr.port,
|
27
|
+
script_name: rr.script_name,
|
28
|
+
path_info: rr.path_info,
|
29
|
+
query_string: rr.query_string,
|
30
|
+
request_body: rr.body,
|
31
|
+
request_headers: Headers.extract(env)
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module WebPipe
|
2
|
+
module ConnSupport
|
3
|
+
# Error raised when trying to fetch an entry in {Conn}'s bag for
|
4
|
+
# an unknown key.
|
5
|
+
class KeyNotFoundInBagError < KeyError
|
6
|
+
# @param key [Any] Key not found in the bag
|
7
|
+
def initialize(key)
|
8
|
+
super(
|
9
|
+
<<~eos
|
10
|
+
Bag does not contain a key with name +key+.
|
11
|
+
eos
|
12
|
+
)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module WebPipe
|
2
|
+
module ConnSupport
|
3
|
+
# Helpers to work with headers and its rack's env representation.
|
4
|
+
#
|
5
|
+
# @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
|
+
# Extracts headers from rack's env.
|
12
|
+
#
|
13
|
+
# Headers are all those pairs which key begins with `HTTP_` plus
|
14
|
+
# those detailed in {HEADERS_AS_CGI}.
|
15
|
+
#
|
16
|
+
# @param env [Types::Env[]]
|
17
|
+
#
|
18
|
+
# @return [Types::Headers[]]
|
19
|
+
#
|
20
|
+
# @see HEADERS_AS_CGI
|
21
|
+
# @see .normalize_key
|
22
|
+
def self.extract(env)
|
23
|
+
Hash[
|
24
|
+
env.
|
25
|
+
select { |k, _v| k.start_with?('HTTP_') }.
|
26
|
+
map { |k, v| pair(k[5 .. -1], v) }.
|
27
|
+
concat(
|
28
|
+
env.
|
29
|
+
select { |k, _v| HEADERS_AS_CGI.include?(k) }.
|
30
|
+
map { |k, v| pair(k, v) }
|
31
|
+
)
|
32
|
+
]
|
33
|
+
end
|
34
|
+
|
35
|
+
# Adds key/value pair to given headers.
|
36
|
+
#
|
37
|
+
# Key is normalized.
|
38
|
+
#
|
39
|
+
# @param headers [Type::Headers[]]
|
40
|
+
# @param key [String]
|
41
|
+
# @param value [String]
|
42
|
+
#
|
43
|
+
# @return [Type::Headers[]]
|
44
|
+
#
|
45
|
+
# @see .normalize_key
|
46
|
+
def self.add(headers, key, value)
|
47
|
+
Hash[
|
48
|
+
headers.to_a.push(pair(key, value))
|
49
|
+
]
|
50
|
+
end
|
51
|
+
|
52
|
+
# Deletes pair with given key form headers.
|
53
|
+
#
|
54
|
+
# Accepts a non normalized key.
|
55
|
+
#
|
56
|
+
# @param headers [Type::Headers[]]
|
57
|
+
# @param key [String]
|
58
|
+
#
|
59
|
+
# @return [Type::Headers[]]
|
60
|
+
#
|
61
|
+
# @see .normalize_key
|
62
|
+
def self.delete(headers, key)
|
63
|
+
headers.reject { |k, _v| normalize_key(key) == k }
|
64
|
+
end
|
65
|
+
|
66
|
+
# Creates a pair with normalized key and raw value.
|
67
|
+
#
|
68
|
+
# @param key [String]
|
69
|
+
# @param key [String]
|
70
|
+
#
|
71
|
+
# @return [Array<String>]
|
72
|
+
#
|
73
|
+
# @see .normalize_key
|
74
|
+
def self.pair(key, value)
|
75
|
+
[normalize_key(key), value]
|
76
|
+
end
|
77
|
+
|
78
|
+
# Normalizes a header key to convention.
|
79
|
+
#
|
80
|
+
# As per RFC2616, headers names are case insensitive. This
|
81
|
+
# function normalizes them to PascalCase acting on dashes ('-').
|
82
|
+
#
|
83
|
+
# When a rack server maps headers to CGI-like variables, both
|
84
|
+
# dashes and underscores (`_`) are treated as dashes. This
|
85
|
+
# function substitutes all '-' to '_'.
|
86
|
+
#
|
87
|
+
# @param key [String]
|
88
|
+
#
|
89
|
+
# @return [String]
|
90
|
+
#
|
91
|
+
# @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
|
92
|
+
def self.normalize_key(key)
|
93
|
+
key.downcase.gsub('_', '-').split('-').map(&:capitalize).join('-')
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'dry/types'
|
2
|
+
require 'rack/request'
|
3
|
+
require 'web_pipe/types'
|
4
|
+
|
5
|
+
module WebPipe
|
6
|
+
module ConnSupport
|
7
|
+
# Types used for {Conn} struct.
|
8
|
+
#
|
9
|
+
# Implementation self-describes them, but you can look at {Conn}
|
10
|
+
# attributes for documentation.
|
11
|
+
module Types
|
12
|
+
include Dry.Types()
|
13
|
+
|
14
|
+
Env = Strict::Hash
|
15
|
+
Request = Instance(::Rack::Request)
|
16
|
+
|
17
|
+
Scheme = Strict::Symbol.enum(:http, :https)
|
18
|
+
Method = Strict::Symbol.enum(
|
19
|
+
:get, :head, :post, :put, :delete, :connect, :options, :trace, :patch
|
20
|
+
)
|
21
|
+
Host = Strict::String
|
22
|
+
Ip = Strict::String.optional
|
23
|
+
Port = Strict::Integer
|
24
|
+
ScriptName = Strict::String
|
25
|
+
PathInfo = Strict::String
|
26
|
+
QueryString = Strict::String
|
27
|
+
RequestBody = WebPipe::Types.Contract(:gets, :each, :read, :rewind)
|
28
|
+
|
29
|
+
BaseUrl = Strict::String
|
30
|
+
Path = Strict::String
|
31
|
+
FullPath = Strict::String
|
32
|
+
Url = Strict::String
|
33
|
+
Params = Strict::Hash
|
34
|
+
|
35
|
+
Status = Strict::Integer.
|
36
|
+
default(200).
|
37
|
+
constrained(gteq: 100, lteq: 599)
|
38
|
+
ResponseBody = WebPipe::Types.Contract(:each).
|
39
|
+
default([''].freeze)
|
40
|
+
|
41
|
+
Headers = Strict::Hash.
|
42
|
+
map(Strict::String, Strict::String).
|
43
|
+
default({}.freeze)
|
44
|
+
|
45
|
+
Bag = Strict::Hash.
|
46
|
+
map(Strict::Symbol, Strict::Any).
|
47
|
+
default({}.freeze)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'dry/initializer'
|
2
|
+
require 'web_pipe/dsl/class_context'
|
3
|
+
require 'web_pipe/dsl/instance_methods'
|
4
|
+
|
5
|
+
module WebPipe
|
6
|
+
module DSL
|
7
|
+
# When an instance of it is included in a module, the module
|
8
|
+
# extends a {ClassContext} instance and includes
|
9
|
+
# {InstanceMethods}.
|
10
|
+
#
|
11
|
+
# @private
|
12
|
+
class Builder < Module
|
13
|
+
# Container with nothing registered.
|
14
|
+
EMPTY_CONTAINER = {}.freeze
|
15
|
+
|
16
|
+
# @!attribute [r] container
|
17
|
+
# @return [Types::Container[]]
|
18
|
+
|
19
|
+
|
20
|
+
include Dry::Initializer.define -> do
|
21
|
+
option :container, type: Types::Container, default: proc { EMPTY_CONTAINER }
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [ClassContext]
|
25
|
+
attr_reader :class_context
|
26
|
+
|
27
|
+
def initialize(*args)
|
28
|
+
super
|
29
|
+
@class_context = ClassContext.new(container: container)
|
30
|
+
end
|
31
|
+
|
32
|
+
def included(klass)
|
33
|
+
klass.extend(class_context)
|
34
|
+
klass.include(InstanceMethods)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'dry-initializer'
|
2
|
+
require 'web_pipe/types'
|
3
|
+
require 'web_pipe/dsl/dsl_context'
|
4
|
+
|
5
|
+
module WebPipe
|
6
|
+
module DSL
|
7
|
+
# Defines the DSL and keeps the state for the pipe.
|
8
|
+
#
|
9
|
+
# This is good to be an instance because it keeps the
|
10
|
+
# configuration (state) for the pipe class: the container
|
11
|
+
# configured on initialization and both rack middlewares and plugs
|
12
|
+
# added through the DSL {DSLContext}.
|
13
|
+
#
|
14
|
+
# As the pipe is extended with an instance of this class, methods
|
15
|
+
# that are meant to be class methods in the pipe are defined as
|
16
|
+
# singleton methods of the instance.
|
17
|
+
#
|
18
|
+
# @private
|
19
|
+
class ClassContext < Module
|
20
|
+
# Methods to be imported from the {DSLContext}.
|
21
|
+
DSL_METHODS = %i[middlewares use plugs plug].freeze
|
22
|
+
|
23
|
+
# @!attribute [r] container
|
24
|
+
# @return [Types::Container[]]
|
25
|
+
|
26
|
+
|
27
|
+
include Dry::Initializer.define -> do
|
28
|
+
option :container, type: Types::Container
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [DSLContext]
|
32
|
+
attr_reader :dsl_context
|
33
|
+
|
34
|
+
def initialize(*args)
|
35
|
+
super
|
36
|
+
@dsl_context = DSLContext.new([], [])
|
37
|
+
define_container
|
38
|
+
define_dsl
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def define_container
|
44
|
+
module_exec(container) do |container|
|
45
|
+
define_method(:container) do
|
46
|
+
container
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def define_dsl
|
52
|
+
DSL_METHODS.each do |method|
|
53
|
+
module_exec(dsl_context) do |dsl_context|
|
54
|
+
define_method(method) do |*args|
|
55
|
+
dsl_context.method(method).(*args)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'dry/initializer'
|
2
|
+
require 'web_pipe/types'
|
3
|
+
require 'web_pipe/plug'
|
4
|
+
require 'web_pipe/rack/middleware'
|
5
|
+
|
6
|
+
module WebPipe
|
7
|
+
module DSL
|
8
|
+
# Defines the DSL for the pipe class and keeps it state.
|
9
|
+
#
|
10
|
+
# This allows adding rack middlewares and plugs at the class
|
11
|
+
# definition level.
|
12
|
+
#
|
13
|
+
# @private
|
14
|
+
class DSLContext
|
15
|
+
# @!attribute middlewares
|
16
|
+
# @return [Array<Rack::Middleware>]
|
17
|
+
|
18
|
+
# @!attribute plugs
|
19
|
+
# @return [Array<Plug>]
|
20
|
+
|
21
|
+
|
22
|
+
include Dry::Initializer.define -> do
|
23
|
+
param :middlewares,
|
24
|
+
type: Types.Array(Rack::Middleware::Instance)
|
25
|
+
|
26
|
+
param :plugs,
|
27
|
+
type: Types.Array(Plug::Instance)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Creates and add a rack middleware to the stack.
|
31
|
+
#
|
32
|
+
# @param middleware
|
33
|
+
# [WebPipe::Rack::Middleware::MiddlewareClass[]] Rack middleware
|
34
|
+
# @param middleware [WebPipe::Rack::Options[]] Options to
|
35
|
+
# initialize
|
36
|
+
#
|
37
|
+
# @return [Array<Rack::Middleware>]
|
38
|
+
def use(middleware, *options)
|
39
|
+
middlewares << Rack::Middleware.new(middleware, options)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Creates and adds a plug to the stack.
|
43
|
+
#
|
44
|
+
# @param name [Plug::Name[]]
|
45
|
+
# @param with [Plug::Spec[]]
|
46
|
+
#
|
47
|
+
# @return [Array<Plug>]
|
48
|
+
def plug(name, with: nil)
|
49
|
+
plugs << Plug.new(name, with)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'dry/initializer'
|
2
|
+
require 'web_pipe/types'
|
3
|
+
require 'web_pipe/conn'
|
4
|
+
require 'web_pipe/app'
|
5
|
+
require 'web_pipe/plug'
|
6
|
+
require 'web_pipe/rack/app_with_middlewares'
|
7
|
+
|
8
|
+
module WebPipe
|
9
|
+
module DSL
|
10
|
+
# Instance methods for the pipe.
|
11
|
+
#
|
12
|
+
# It is from here that you get the rack application you can route
|
13
|
+
# to. The initialization phase gives you the chance to inject any
|
14
|
+
# of the plugs, while the instance you get has the `#call` method
|
15
|
+
# expected by rack.
|
16
|
+
#
|
17
|
+
# The pipe state can be accessed through the pipe class, which
|
18
|
+
# has been configured through {ClassContext}.
|
19
|
+
#
|
20
|
+
# @private
|
21
|
+
module InstanceMethods
|
22
|
+
# No injections at all.
|
23
|
+
EMPTY_INJECTIONS = {}.freeze
|
24
|
+
|
25
|
+
# Type for how plugs should be injected.
|
26
|
+
Injections = Types::Strict::Hash.map(Plug::Name, Plug::Spec)
|
27
|
+
|
28
|
+
# @!attribute [r] injections [Injections[]]
|
29
|
+
# Injected plugs that allow overriding what has been configured.
|
30
|
+
|
31
|
+
|
32
|
+
include Dry::Initializer.define -> do
|
33
|
+
param :injections,
|
34
|
+
default: proc { EMPTY_INJECTIONS },
|
35
|
+
type: Injections
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [Rack::AppWithMiddlewares]
|
39
|
+
attr_reader :rack_app
|
40
|
+
|
41
|
+
def initialize(*args)
|
42
|
+
super
|
43
|
+
middlewares = self.class.middlewares
|
44
|
+
container = self.class.container
|
45
|
+
operations = Plug.inject_and_resolve(self.class.plugs, injections, container, self)
|
46
|
+
app = App.new(operations)
|
47
|
+
@rack_app = Rack::AppWithMiddlewares.new(middlewares, app)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Expected interface for rack.
|
51
|
+
#
|
52
|
+
# @param env [Hash] Rack env
|
53
|
+
#
|
54
|
+
# @return [Array] Rack response
|
55
|
+
def call(env)
|
56
|
+
rack_app.call(env)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'dry/initializer'
|
2
|
+
require 'web_pipe/types'
|
3
|
+
require 'web_pipe/app'
|
4
|
+
|
5
|
+
module WebPipe
|
6
|
+
# A plug is a specification to resolve a callable object.
|
7
|
+
#
|
8
|
+
# It is initialized with a {Name} and a {Spec} and, on resolution
|
9
|
+
# time, is called with a {Types::Container} and an {Object} to act
|
10
|
+
# in the following fashion:
|
11
|
+
#
|
12
|
+
# - When the spec responds to `#call`, it is returned itself as the
|
13
|
+
# callable object.
|
14
|
+
# - When the spec is `nil`, then a {Proc} wrapping a method with the
|
15
|
+
# plug name in `object` is returned.
|
16
|
+
# - Otherwise, spec is taken as the key to resolve the operation
|
17
|
+
# from the `container`.
|
18
|
+
#
|
19
|
+
# @private
|
20
|
+
class Plug
|
21
|
+
# Error raised when no operation can be resolved from a {Spec}.
|
22
|
+
class InvalidPlugError < ArgumentError
|
23
|
+
# @param name [Any] Name for the plug that can't be resolved
|
24
|
+
def initialize(name)
|
25
|
+
super(
|
26
|
+
<<~eos
|
27
|
+
Plug with name +#{name}+ is invalid. It must be something
|
28
|
+
callable, an instance method when `with:` is not given, or
|
29
|
+
something callable registered in the container."
|
30
|
+
eos
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Type for the name of a plug.
|
36
|
+
Name = Types::Strict::Symbol.constructor(&:to_sym)
|
37
|
+
|
38
|
+
# Type for the spec to resolve and {App::Operation} on a
|
39
|
+
# {Conn} used by {Plug}.
|
40
|
+
Spec = App::Operation | Types.Constant(nil) | Types::Strict::String | Types::Strict::Symbol
|
41
|
+
|
42
|
+
# Type for an instance of self.
|
43
|
+
Instance = Types.Instance(self)
|
44
|
+
|
45
|
+
# @!attribute [r] name
|
46
|
+
# @return [Name[]]
|
47
|
+
|
48
|
+
# @!attribute [r] spec
|
49
|
+
# @return [Spec[]]
|
50
|
+
|
51
|
+
|
52
|
+
include Dry::Initializer.define -> do
|
53
|
+
param :name, Name
|
54
|
+
|
55
|
+
param :spec, Spec
|
56
|
+
end
|
57
|
+
|
58
|
+
# Creates a new instance with given `spec` but keeping `name`.
|
59
|
+
#
|
60
|
+
# @param new_spec [Spec[]]
|
61
|
+
# @return [self]
|
62
|
+
def with(new_spec)
|
63
|
+
self.class.new(name, new_spec)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Resolves the operation.
|
67
|
+
#
|
68
|
+
# @param container [Types::Container[]]
|
69
|
+
# @param object [Object]
|
70
|
+
#
|
71
|
+
# @return [Operation[]]
|
72
|
+
# @raise [InvalidPlugError] When nothing callable is resolved.
|
73
|
+
def call(container, pipe)
|
74
|
+
if spec.respond_to?(:call)
|
75
|
+
spec
|
76
|
+
elsif spec.nil?
|
77
|
+
pipe.method(name)
|
78
|
+
elsif container[spec] && container[spec].respond_to?(:call)
|
79
|
+
container[spec]
|
80
|
+
else
|
81
|
+
raise InvalidPlugError.new(name)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Change `plugs` spec's present in `injections` and resolves.
|
86
|
+
#
|
87
|
+
# @param plugs [Array<Plug>]
|
88
|
+
# @param injections [InstanceMethods::Injections[]]
|
89
|
+
# @container container [Types::Container[]]
|
90
|
+
# @object [Object]
|
91
|
+
#
|
92
|
+
# @return [Array<Operation[]>]
|
93
|
+
def self.inject_and_resolve(plugs, injections, container, object)
|
94
|
+
plugs.map do |plug|
|
95
|
+
if injections.has_key?(plug.name)
|
96
|
+
plug.with(injections[plug.name])
|
97
|
+
else
|
98
|
+
plug
|
99
|
+
end.(container, object)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|