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