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