hanami 2.0.0.alpha1 → 2.0.0.alpha5

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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +306 -5
  3. data/FEATURES.md +9 -1
  4. data/LICENSE.md +1 -1
  5. data/README.md +9 -6
  6. data/hanami.gemspec +12 -11
  7. data/lib/hanami/application/autoloader/inflector_adapter.rb +22 -0
  8. data/lib/hanami/application/container/boot/inflector.rb +7 -0
  9. data/lib/hanami/application/container/boot/logger.rb +7 -0
  10. data/lib/hanami/application/container/boot/rack_logger.rb +19 -0
  11. data/lib/hanami/application/container/boot/rack_monitor.rb +12 -0
  12. data/lib/hanami/application/container/boot/routes_helper.rb +9 -0
  13. data/lib/hanami/application/container/boot/settings.rb +7 -0
  14. data/lib/hanami/application/router.rb +59 -0
  15. data/lib/hanami/application/routes.rb +55 -0
  16. data/lib/hanami/application/routes_helper.rb +34 -0
  17. data/lib/hanami/application/routing/middleware/stack.rb +89 -0
  18. data/lib/hanami/application/routing/resolver/node.rb +50 -0
  19. data/lib/hanami/application/routing/resolver/trie.rb +59 -0
  20. data/lib/hanami/application/routing/resolver.rb +87 -0
  21. data/lib/hanami/application/routing/router.rb +36 -0
  22. data/lib/hanami/application/settings/dotenv_store.rb +60 -0
  23. data/lib/hanami/application/settings.rb +93 -0
  24. data/lib/hanami/application.rb +330 -34
  25. data/lib/hanami/assets/application_configuration.rb +63 -0
  26. data/lib/hanami/assets/configuration.rb +54 -0
  27. data/lib/hanami/boot/source_dirs.rb +44 -0
  28. data/lib/hanami/boot.rb +1 -2
  29. data/lib/hanami/cli/application/cli.rb +40 -0
  30. data/lib/hanami/cli/application/command.rb +47 -0
  31. data/lib/hanami/cli/application/commands/console.rb +81 -0
  32. data/lib/hanami/cli/application/commands.rb +16 -0
  33. data/lib/hanami/cli/base_command.rb +48 -0
  34. data/lib/hanami/cli/commands/command.rb +4 -4
  35. data/lib/hanami/cli/commands.rb +3 -2
  36. data/lib/hanami/configuration/logger.rb +84 -0
  37. data/lib/hanami/configuration/middleware.rb +4 -4
  38. data/lib/hanami/configuration/null_configuration.rb +14 -0
  39. data/lib/hanami/configuration/router.rb +52 -0
  40. data/lib/hanami/configuration/sessions.rb +5 -5
  41. data/lib/hanami/configuration/source_dirs.rb +42 -0
  42. data/lib/hanami/configuration.rb +122 -131
  43. data/lib/hanami/init.rb +5 -0
  44. data/lib/hanami/setup.rb +9 -0
  45. data/lib/hanami/slice.rb +189 -0
  46. data/lib/hanami/version.rb +1 -1
  47. data/lib/hanami/web/rack_logger.rb +96 -0
  48. data/lib/hanami.rb +17 -30
  49. metadata +116 -50
  50. data/bin/hanami +0 -8
  51. data/lib/hanami/configuration/cookies.rb +0 -24
  52. data/lib/hanami/configuration/security.rb +0 -141
  53. data/lib/hanami/container.rb +0 -107
  54. data/lib/hanami/frameworks.rb +0 -28
  55. data/lib/hanami/routes.rb +0 -31
@@ -3,7 +3,16 @@
3
3
  require "uri"
4
4
  require "concurrent/hash"
5
5
  require "concurrent/array"
6
+ require "dry/configurable"
6
7
  require "dry/inflector"
8
+ require "pathname"
9
+
10
+ require_relative "application/settings/dotenv_store"
11
+ require_relative "configuration/logger"
12
+ require_relative "configuration/middleware"
13
+ require_relative "configuration/router"
14
+ require_relative "configuration/sessions"
15
+ require_relative "configuration/source_dirs"
7
16
 
8
17
  module Hanami
9
18
  # Hanami application configuration
@@ -12,142 +21,149 @@ module Hanami
12
21
  #
13
22
  # rubocop:disable Metrics/ClassLength
14
23
  class Configuration
15
- require "hanami/configuration/cookies"
16
- require "hanami/configuration/sessions"
17
- require "hanami/configuration/middleware"
18
- require "hanami/configuration/security"
24
+ include Dry::Configurable
25
+
26
+ DEFAULT_ENVIRONMENTS = Concurrent::Hash.new { |h, k| h[k] = Concurrent::Array.new }
27
+ private_constant :DEFAULT_ENVIRONMENTS
19
28
 
20
- # rubocop:disable Metrics/MethodLength
21
- def initialize(env:)
22
- @settings = Concurrent::Hash.new
29
+ MODULE_DELIMITER = "::"
30
+ private_constant :MODULE_DELIMITER
23
31
 
24
- self.env = env
25
- self.environments = DEFAULT_ENVIRONMENTS.clone
32
+ attr_reader :actions
33
+ attr_reader :middleware
34
+ attr_reader :router
35
+ attr_reader :views, :assets
26
36
 
27
- self.base_url = DEFAULT_BASE_URL
37
+ attr_reader :environments
38
+ private :environments
28
39
 
29
- self.logger = DEFAULT_LOGGER.clone
30
- self.routes = DEFAULT_ROUTES
31
- self.cookies = DEFAULT_COOKIES
32
- self.sessions = DEFAULT_SESSIONS
40
+ def initialize(application_name:, env:)
41
+ @namespace = application_name.split(MODULE_DELIMITER)[0..-2].join(MODULE_DELIMITER)
33
42
 
34
- self.default_request_format = DEFAULT_REQUEST_FORMAT
35
- self.default_response_format = DEFAULT_RESPONSE_FORMAT
43
+ @environments = DEFAULT_ENVIRONMENTS.clone
44
+ config.env = env
36
45
 
37
- self.middleware = Middleware.new
38
- self.security = Security.new
46
+ # Some default setting values must be assigned at initialize-time to ensure they
47
+ # have appropriate values for the current application
48
+ self.root = Dir.pwd
49
+ self.settings_store = Application::Settings::DotenvStore.new.with_dotenv_loaded
39
50
 
40
- self.inflections = Dry::Inflector.new
41
- end
42
- # rubocop:enable Metrics/MethodLength
51
+ config.logger = Configuration::Logger.new(env: env, application_name: method(:application_name))
43
52
 
44
- def finalize
45
- environment_for(env).each do |blk|
46
- instance_eval(&blk)
53
+ @assets = begin
54
+ require_path = "hanami/assets/application_configuration"
55
+ require require_path
56
+ Hanami::Assets::ApplicationConfiguration.new
57
+ rescue LoadError => e
58
+ raise e unless e.path == require_path
59
+ require_relative "configuration/null_configuration"
60
+ NullConfiguration.new
47
61
  end
48
62
 
49
- self
50
- end
63
+ # Config for actions (same for views, below) may not be available if the gem isn't
64
+ # loaded; fall back to a null config object if it's missing
65
+ @actions = begin
66
+ require_path = "hanami/action/application_configuration"
67
+ require require_path
68
+ Hanami::Action::ApplicationConfiguration.new(assets_server_url: assets.server_url)
69
+ rescue LoadError => e
70
+ raise e unless e.path == require_path
71
+ require_relative "configuration/null_configuration"
72
+ NullConfiguration.new
73
+ end
51
74
 
52
- def environment(name, &blk)
53
- environment_for(name).push(blk)
54
- end
75
+ @middleware = Middleware.new
55
76
 
56
- def env=(value)
57
- settings[:env] = value
58
- end
77
+ @router = Router.new(self)
59
78
 
60
- def env
61
- settings.fetch(:env)
62
- end
79
+ @views = begin
80
+ require_path = "hanami/view/application_configuration"
81
+ require require_path
82
+ Hanami::View::ApplicationConfiguration.new
83
+ rescue LoadError => e
84
+ raise e unless e.path == require_path
85
+ require_relative "configuration/null_configuration"
86
+ NullConfiguration.new
87
+ end
63
88
 
64
- def base_url=(value)
65
- settings[:base_url] = URI.parse(value)
89
+ yield self if block_given?
66
90
  end
67
91
 
68
- def base_url
69
- settings.fetch(:base_url)
70
- end
92
+ def environment(env_name, &block)
93
+ environments[env_name] << block
94
+ apply_env_config
71
95
 
72
- def logger=(options)
73
- settings[:logger] = options
96
+ self
74
97
  end
75
98
 
76
- def logger
77
- settings.fetch(:logger)
78
- end
99
+ def finalize!
100
+ apply_env_config
79
101
 
80
- def routes=(value)
81
- settings[:routes] = value
82
- end
102
+ # Finalize nested configurations
103
+ assets.finalize!
104
+ actions.finalize!
105
+ views.finalize!
106
+ logger.finalize!
107
+ router.finalize!
83
108
 
84
- def routes
85
- settings.fetch(:routes)
109
+ super
86
110
  end
87
111
 
88
- def cookies=(options)
89
- settings[:cookies] = Cookies.new(options)
112
+ def namespace
113
+ inflector.constantize(@namespace)
90
114
  end
91
115
 
92
- def cookies
93
- settings.fetch(:cookies)
116
+ def application_name
117
+ inflector.underscore(@namespace).to_sym
94
118
  end
95
119
 
96
- def sessions=(*args)
97
- settings[:sessions] = Sessions.new(args)
98
- end
120
+ setting :env
99
121
 
100
- def sessions
101
- settings.fetch(:sessions)
122
+ def env=(new_env)
123
+ config.env = env
124
+ apply_env_config(new_env)
102
125
  end
103
126
 
104
- def default_request_format=(value)
105
- settings[:default_request_format] = value
106
- end
127
+ setting :root, constructor: -> path { Pathname(path) }
107
128
 
108
- def default_request_format
109
- settings.fetch(:default_request_format)
110
- end
129
+ setting :inflector, default: Dry::Inflector.new, cloneable: true
111
130
 
112
- def default_response_format=(value)
113
- settings[:default_response_format] = value
131
+ def inflections(&block)
132
+ self.inflector = Dry::Inflector.new(&block)
114
133
  end
115
134
 
116
- def default_response_format
117
- settings.fetch(:default_response_format)
118
- end
135
+ setting :logger, cloneable: true
119
136
 
120
- def middleware
121
- settings.fetch(:middleware)
137
+ def logger=(logger_instance)
138
+ @logger_instance = logger_instance
122
139
  end
123
140
 
124
- def security=(value)
125
- settings[:security] = value
141
+ def logger_instance
142
+ @logger_instance || logger.instance
126
143
  end
127
144
 
128
- def security
129
- settings.fetch(:security)
130
- end
145
+ setting :settings_path, default: File.join("config", "settings")
131
146
 
132
- def inflections(&blk)
133
- if blk.nil?
134
- settings.fetch(:inflections)
135
- else
136
- settings[:inflections] = Dry::Inflector.new(&blk)
137
- end
138
- end
147
+ setting :settings_class_name, default: "Settings"
139
148
 
140
- def router_settings
141
- bu = base_url
149
+ setting :settings_store, default: Application::Settings::DotenvStore
142
150
 
143
- {
144
- scheme: bu.scheme,
145
- host: bu.host,
146
- port: bu.port,
147
- inflector: inflections
148
- }
151
+ setting :slices_dir, default: "slices"
152
+
153
+ setting :slices_namespace, default: Object
154
+
155
+ # TODO: convert into a dedicated object with explicit behaviour around blocks per
156
+ # slice, etc.
157
+ setting :slices, default: {}, constructor: :dup.to_proc
158
+
159
+ setting :source_dirs, default: Configuration::SourceDirs.new, cloneable: true
160
+
161
+ def slice(slice_name, &block)
162
+ slices[slice_name] = block
149
163
  end
150
164
 
165
+ setting :base_url, default: "http://0.0.0.0:2300", constructor: -> url { URI(url) }
166
+
151
167
  def for_each_middleware(&blk)
152
168
  stack = middleware.stack.dup
153
169
  stack += sessions.middleware if sessions.enabled?
@@ -155,51 +171,26 @@ module Hanami
155
171
  stack.each(&blk)
156
172
  end
157
173
 
158
- protected
174
+ setting :sessions, default: :null, constructor: -> *args { Sessions.new(*args) }
159
175
 
160
- def environment_for(name)
161
- settings[:environments][name]
162
- end
176
+ private
163
177
 
164
- def environments=(values)
165
- settings[:environments] = values
178
+ def apply_env_config(env = self.env)
179
+ environments[env].each do |block|
180
+ instance_eval(&block)
181
+ end
166
182
  end
167
183
 
168
- def middleware=(value)
169
- settings[:middleware] = value
184
+ def method_missing(name, *args, &block)
185
+ if config.respond_to?(name)
186
+ config.public_send(name, *args, &block)
187
+ else
188
+ super
189
+ end
170
190
  end
171
191
 
172
- def inflections=(value)
173
- settings[:inflections] = value
192
+ def respond_to_missing?(name, _incude_all = false)
193
+ config.respond_to?(name) || super
174
194
  end
175
-
176
- private
177
-
178
- DEFAULT_ENVIRONMENTS = Concurrent::Hash.new { |h, k| h[k] = Concurrent::Array.new }
179
- private_constant :DEFAULT_ENVIRONMENTS
180
-
181
- DEFAULT_BASE_URL = "http://0.0.0.0:2300"
182
- private_constant :DEFAULT_BASE_URL
183
-
184
- DEFAULT_LOGGER = { level: :debug }.freeze
185
- private_constant :DEFAULT_LOGGER
186
-
187
- DEFAULT_ROUTES = File.join("config", "routes")
188
- private_constant :DEFAULT_ROUTES
189
-
190
- DEFAULT_COOKIES = Cookies.null
191
- private_constant :DEFAULT_COOKIES
192
-
193
- DEFAULT_SESSIONS = Sessions.null
194
- private_constant :DEFAULT_SESSIONS
195
-
196
- DEFAULT_REQUEST_FORMAT = :html
197
- private_constant :DEFAULT_REQUEST_FORMAT
198
-
199
- DEFAULT_RESPONSE_FORMAT = :html
200
- private_constant :DEFAULT_RESPONSE_FORMAT
201
-
202
- attr_reader :settings
203
195
  end
204
- # rubocop:enable Metrics/ClassLength
205
196
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "setup"
4
+
5
+ Hanami.init
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "hanami"
5
+
6
+ begin
7
+ require File.join(Dir.pwd, "config/application")
8
+ rescue LoadError # rubocop:disable Lint/SuppressedException
9
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/system/container"
4
+ require "dry/system/loader/autoloading"
5
+ require "pathname"
6
+
7
+ module Hanami
8
+ # Distinct area of concern within an Hanami application
9
+ #
10
+ # @since 2.0.0
11
+ class Slice
12
+ attr_reader :application, :name, :namespace, :root
13
+
14
+ def initialize(application, name:, namespace: nil, root: nil, container: nil)
15
+ @application = application
16
+ @name = name.to_sym
17
+ @namespace = namespace
18
+ @root = root ? Pathname(root) : root
19
+ @container = container || define_container
20
+ end
21
+
22
+ def inflector
23
+ application.inflector
24
+ end
25
+
26
+ def namespace_path
27
+ @namespace_path ||= inflector.underscore(namespace.to_s)
28
+ end
29
+
30
+ def init
31
+ container.import application: application.container
32
+
33
+ slice_block = application.configuration.slices[name]
34
+ instance_eval(&slice_block) if slice_block
35
+ end
36
+
37
+ def boot
38
+ container.finalize! do
39
+ container.config.env = application.container.config.env
40
+ end
41
+
42
+ @booted = true
43
+ self
44
+ end
45
+
46
+ # rubocop:disable Style/DoubleNegation
47
+ def booted?
48
+ !!@booted
49
+ end
50
+ # rubocop:enable Style/DoubleNegation
51
+
52
+ def container
53
+ @container ||= define_container
54
+ end
55
+
56
+ def import(*slice_names)
57
+ raise "Cannot import after booting" if booted?
58
+
59
+ slice_names.each do |slice_name|
60
+ container.import slice_name.to_sym => application.slices.fetch(slice_name.to_sym).container
61
+ end
62
+ end
63
+
64
+ def register(*args, &block)
65
+ container.register(*args, &block)
66
+ end
67
+
68
+ def register_bootable(*args, &block)
69
+ container.boot(*args, &block)
70
+ end
71
+
72
+ def init_bootable(*args)
73
+ container.init(*args)
74
+ end
75
+
76
+ def start_bootable(*args)
77
+ container.start(*args)
78
+ end
79
+
80
+ def key?(*args)
81
+ container.key?(*args)
82
+ end
83
+
84
+ def keys
85
+ container.keys
86
+ end
87
+
88
+ def [](*args)
89
+ container[*args]
90
+ end
91
+
92
+ def resolve(*args)
93
+ container.resolve(*args)
94
+ end
95
+
96
+ private
97
+
98
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
99
+ def define_container
100
+ container = Class.new(Dry::System::Container)
101
+ container.use :env
102
+
103
+ container.configure do |config|
104
+ config.name = name
105
+ config.inflector = application.configuration.inflector
106
+
107
+ config.component_dirs.loader = Dry::System::Loader::Autoloading
108
+ config.component_dirs.add_to_load_path = false
109
+
110
+ if root&.directory?
111
+ config.root = root
112
+ config.bootable_dirs = ["config/boot"]
113
+
114
+ # Add component dirs for each configured component path
115
+ application.configuration.source_dirs.component_dirs.each do |component_dir|
116
+ next unless root.join(component_dir.path).directory?
117
+
118
+ component_dir = component_dir.dup
119
+
120
+ # TODO: this `== "lib"` check should be codified into a method somewhere
121
+ if component_dir.path == "lib"
122
+ # Expect component files in the root of the lib/ component dir to define
123
+ # classes inside the slice's namespace.
124
+ #
125
+ # e.g. "lib/foo.rb" should define SliceNamespace::Foo, to be registered as
126
+ # "foo"
127
+ component_dir.namespaces.delete_root
128
+ component_dir.namespaces.add_root(key: nil, const: namespace_path)
129
+
130
+ config.component_dirs.add(component_dir)
131
+
132
+ application.autoloader.push_dir(root.join("lib"), namespace: namespace)
133
+ else
134
+ # Expect component files in the root of non-lib/ component dirs to define
135
+ # classes inside a namespace matching that dir.
136
+ #
137
+ # e.g. "actions/foo.rb" should define SliceNamespace::Actions::Foo, to be
138
+ # registered as "actions.foo"
139
+
140
+ dir_namespace_path = File.join(namespace_path, component_dir.path)
141
+
142
+ autoloader_namespace = begin
143
+ inflector.constantize(inflector.camelize(dir_namespace_path))
144
+ rescue NameError
145
+ namespace.const_set(inflector.camelize(component_dir.path), Module.new)
146
+ end
147
+
148
+ component_dir.namespaces.delete_root
149
+ component_dir.namespaces.add_root(const: dir_namespace_path, key: component_dir.path) # TODO: do we need to swap path delimiters for key delimiters here?
150
+
151
+ config.component_dirs.add(component_dir)
152
+
153
+ application.autoloader.push_dir(
154
+ container.root.join(component_dir.path),
155
+ namespace: autoloader_namespace
156
+ )
157
+ end
158
+ end
159
+
160
+ # Pass configured autoload dirs to the autoloader
161
+ application.configuration.source_dirs.autoload_paths.each do |autoload_path|
162
+ next unless root.join(autoload_path).directory?
163
+
164
+ dir_namespace_path = File.join(namespace_path, autoload_path)
165
+
166
+ autoloader_namespace = begin
167
+ inflector.constantize(inflector.camelize(dir_namespace_path))
168
+ rescue NameError
169
+ namespace.const_set(inflector.camelize(autoload_path), Module.new)
170
+ end
171
+
172
+ application.autoloader.push_dir(
173
+ container.root.join(autoload_path),
174
+ namespace: autoloader_namespace
175
+ )
176
+ end
177
+ end
178
+ end
179
+
180
+ if namespace
181
+ namespace.const_set :Container, container
182
+ namespace.const_set :Deps, container.injector
183
+ end
184
+
185
+ container
186
+ end
187
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
188
+ end
189
+ end
@@ -8,7 +8,7 @@ module Hanami
8
8
  module Version
9
9
  # @since 0.9.0
10
10
  # @api private
11
- VERSION = "2.0.0.alpha1"
11
+ VERSION = "2.0.0.alpha5"
12
12
 
13
13
  # @since 0.9.0
14
14
  # @api private
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "rack/request"
5
+ require "hanami/utils/hash"
6
+
7
+ module Hanami
8
+ module Web
9
+ # Rack logger for Hanami applications
10
+ class RackLogger
11
+ attr_reader :logger
12
+ attr_reader :filter_params
13
+
14
+ def initialize(logger, filter_params: [])
15
+ @logger = logger
16
+ @filter_params = filter_params
17
+ end
18
+
19
+ def attach(rack_monitor)
20
+ rack_monitor.on :stop do |event|
21
+ log_request event[:env], event[:status], event[:time]
22
+ end
23
+
24
+ rack_monitor.on :error do |event|
25
+ log_exception event[:exception]
26
+ end
27
+ end
28
+
29
+ # rubocop:disable Metrics/MethodLength
30
+ def log_request(env, status, time)
31
+ data = {
32
+ http: env[HTTP_VERSION],
33
+ verb: env[REQUEST_METHOD],
34
+ status: status,
35
+ ip: env[HTTP_X_FORWARDED_FOR] || env[REMOTE_ADDR],
36
+ path: env[SCRIPT_NAME] + env[PATH_INFO].to_s,
37
+ length: extract_content_length(env),
38
+ params: extract_params(env),
39
+ elapsed: time,
40
+ }
41
+
42
+ logger.info JSON.generate(data)
43
+ end
44
+ # rubocop:enable Metrics/MethodLength
45
+
46
+ def log_exception(exception)
47
+ logger.error exception.message
48
+ logger.error exception.backtrace.join("\n")
49
+ end
50
+
51
+ private
52
+
53
+ HTTP_VERSION = "HTTP_VERSION"
54
+ REQUEST_METHOD = "REQUEST_METHOD"
55
+ HTTP_X_FORWARDED_FOR = "HTTP_X_FORWARDED_FOR"
56
+ REMOTE_ADDR = "REMOTE_ADDR"
57
+ SCRIPT_NAME = "SCRIPT_NAME"
58
+ PATH_INFO = "PATH_INFO"
59
+ RACK_ERRORS = "rack.errors"
60
+ QUERY_HASH = "rack.request.query_hash"
61
+ FORM_HASH = "rack.request.form_hash"
62
+ ROUTER_PARAMS = "router.params"
63
+ CONTENT_LENGTH = "Content-Length"
64
+
65
+ def extract_content_length(env)
66
+ value = env[CONTENT_LENGTH]
67
+ !value || value.to_s == "0" ? "-" : value
68
+ end
69
+
70
+ def extract_params(env)
71
+ result = env.fetch(QUERY_HASH, {})
72
+ result.merge!(env.fetch(FORM_HASH, {}))
73
+ result.merge!(Hanami::Utils::Hash.deep_stringify(env.fetch(ROUTER_PARAMS, {})))
74
+ result
75
+ end
76
+
77
+ FILTERED = "[FILTERED]"
78
+
79
+ # rubocop:disable Metrics/MethodLength
80
+ def filter(params)
81
+ params.each_with_object({}) do |(k, v), h|
82
+ if filter_params.include?(k)
83
+ h.update(k => FILTERED)
84
+ elsif v.is_a?(Hash)
85
+ h.update(k => filter(v))
86
+ elsif v.is_a?(Array)
87
+ h.update(k => v.map { |m| m.is_a?(Hash) ? filter(m) : m })
88
+ else
89
+ h[k] = v
90
+ end
91
+ end
92
+ end
93
+ # rubocop:enable Metrics/MethodLength
94
+ end
95
+ end
96
+ end