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