durable_parameters 0.2.3

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 (56) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +853 -0
  4. data/Rakefile +29 -0
  5. data/app/params/account_params.rb.example +38 -0
  6. data/app/params/application_params.rb +16 -0
  7. data/lib/durable_parameters/adapters/hanami.rb +138 -0
  8. data/lib/durable_parameters/adapters/rage.rb +124 -0
  9. data/lib/durable_parameters/adapters/rails.rb +280 -0
  10. data/lib/durable_parameters/adapters/sinatra.rb +91 -0
  11. data/lib/durable_parameters/core/application_params.rb +334 -0
  12. data/lib/durable_parameters/core/configuration.rb +83 -0
  13. data/lib/durable_parameters/core/forbidden_attributes_protection.rb +48 -0
  14. data/lib/durable_parameters/core/parameters.rb +643 -0
  15. data/lib/durable_parameters/core/params_registry.rb +110 -0
  16. data/lib/durable_parameters/core.rb +15 -0
  17. data/lib/durable_parameters/log_subscriber.rb +34 -0
  18. data/lib/durable_parameters/railtie.rb +65 -0
  19. data/lib/durable_parameters/version.rb +7 -0
  20. data/lib/durable_parameters.rb +41 -0
  21. data/lib/generators/rails/USAGE +12 -0
  22. data/lib/generators/rails/durable_parameters_controller_generator.rb +17 -0
  23. data/lib/generators/rails/templates/controller.rb +94 -0
  24. data/lib/legacy/action_controller/application_params.rb +235 -0
  25. data/lib/legacy/action_controller/parameters.rb +524 -0
  26. data/lib/legacy/action_controller/params_registry.rb +108 -0
  27. data/lib/legacy/active_model/forbidden_attributes_protection.rb +40 -0
  28. data/test/action_controller_required_params_test.rb +36 -0
  29. data/test/action_controller_tainted_params_test.rb +29 -0
  30. data/test/active_model_mass_assignment_taint_protection_test.rb +25 -0
  31. data/test/application_params_array_test.rb +245 -0
  32. data/test/application_params_edge_cases_test.rb +361 -0
  33. data/test/application_params_test.rb +893 -0
  34. data/test/controller_generator_test.rb +31 -0
  35. data/test/core_parameters_test.rb +2376 -0
  36. data/test/durable_parameters_test.rb +115 -0
  37. data/test/enhanced_error_messages_test.rb +120 -0
  38. data/test/gemfiles/Gemfile.rails-3.0.x +14 -0
  39. data/test/gemfiles/Gemfile.rails-3.1.x +14 -0
  40. data/test/gemfiles/Gemfile.rails-3.2.x +14 -0
  41. data/test/log_on_unpermitted_params_test.rb +49 -0
  42. data/test/metadata_validation_test.rb +294 -0
  43. data/test/multi_parameter_attributes_test.rb +38 -0
  44. data/test/parameters_core_methods_test.rb +503 -0
  45. data/test/parameters_integration_test.rb +553 -0
  46. data/test/parameters_permit_test.rb +491 -0
  47. data/test/parameters_require_test.rb +9 -0
  48. data/test/parameters_taint_test.rb +98 -0
  49. data/test/params_registry_concurrency_test.rb +422 -0
  50. data/test/params_registry_test.rb +112 -0
  51. data/test/permit_by_model_test.rb +227 -0
  52. data/test/raise_on_unpermitted_params_test.rb +32 -0
  53. data/test/test_helper.rb +38 -0
  54. data/test/transform_params_edge_cases_test.rb +526 -0
  55. data/test/transformation_test.rb +360 -0
  56. metadata +223 -0
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sinatra/base'
4
+ require 'durable_parameters/core'
5
+
6
+ module StrongParameters
7
+ module Adapters
8
+ # Sinatra adapter for Strong Parameters
9
+ #
10
+ # This adapter integrates the core Strong Parameters functionality with
11
+ # Sinatra applications, providing a simple params wrapper and error handling.
12
+ #
13
+ # @example Basic usage in a Sinatra app
14
+ # require 'sinatra/base'
15
+ # require 'durable_parameters/adapters/sinatra'
16
+ #
17
+ # class MyApp < Sinatra::Base
18
+ # register StrongParameters::Adapters::Sinatra
19
+ #
20
+ # post '/users' do
21
+ # user_params = strong_params.require(:user).permit(:name, :email)
22
+ # # ... use user_params
23
+ # end
24
+ # end
25
+ module Sinatra
26
+ # Sinatra-specific Parameters implementation
27
+ class Parameters < StrongParameters::Core::Parameters
28
+ # Sinatra typically uses string keys, so we normalize to strings
29
+ private
30
+
31
+ def normalize_key(key)
32
+ key.to_s
33
+ end
34
+ end
35
+
36
+ # Module to register with Sinatra applications
37
+ module Helpers
38
+ # Access request parameters as a Strong Parameters object.
39
+ #
40
+ # @return [Parameters] the request parameters wrapped in a Parameters object
41
+ def strong_params
42
+ @_strong_params ||= ::StrongParameters::Adapters::Sinatra::Parameters.new(params)
43
+ end
44
+
45
+ # Alias for strong_params for Rails compatibility
46
+ alias sp strong_params
47
+ end
48
+
49
+ # Error handler for ParameterMissing
50
+ module ErrorHandlers
51
+ def self.registered(app)
52
+ app.error StrongParameters::Core::ParameterMissing do
53
+ halt 400, { error: "Required parameter missing: #{env['sinatra.error'].param}" }.to_json
54
+ end
55
+
56
+ app.error StrongParameters::Core::ForbiddenAttributes do
57
+ halt 400, { error: "Forbidden attributes in mass assignment" }.to_json
58
+ end
59
+
60
+ app.error StrongParameters::Core::UnpermittedParameters do
61
+ halt 400, { error: "Unpermitted parameters: #{env['sinatra.error'].params.join(', ')}" }.to_json
62
+ end
63
+ end
64
+ end
65
+
66
+ # Register the adapter with a Sinatra application
67
+ def self.registered(app)
68
+ app.helpers Helpers
69
+ app.register ErrorHandlers
70
+
71
+ # Configure logging for unpermitted parameters in development
72
+ if app.development?
73
+ ::StrongParameters::Adapters::Sinatra::Parameters.action_on_unpermitted_parameters = :log
74
+ ::StrongParameters::Adapters::Sinatra::Parameters.unpermitted_notification_handler = lambda do |keys|
75
+ app.logger.warn "Unpermitted parameters: #{keys.join(', ')}" if app.respond_to?(:logger)
76
+ end
77
+ end
78
+ end
79
+
80
+ # Convenience method for class-level registration
81
+ def self.included(base)
82
+ base.register self if base.respond_to?(:register)
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ # Register with Sinatra::Base if it's loaded
89
+ if defined?(::Sinatra::Base)
90
+ ::Sinatra.register StrongParameters::Adapters::Sinatra
91
+ end
@@ -0,0 +1,334 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module StrongParameters
6
+ module Core
7
+ # Base class for declarative parameter permission definitions.
8
+ #
9
+ # ApplicationParams provides a DSL for defining which attributes are allowed
10
+ # or denied for mass assignment. This enables a centralized, declarative
11
+ # approach to parameter filtering that's more maintainable than inline
12
+ # permit() calls.
13
+ #
14
+ # @example Basic usage
15
+ # class UserParams < StrongParameters::Core::ApplicationParams
16
+ # allow :name
17
+ # allow :email
18
+ # deny :is_admin
19
+ # end
20
+ #
21
+ # @example With action-specific permissions
22
+ # class PostParams < StrongParameters::Core::ApplicationParams
23
+ # allow :title
24
+ # allow :body
25
+ # allow :published, only: :create
26
+ # allow :view_count, except: :create
27
+ # end
28
+ #
29
+ # @example With metadata declaration
30
+ # class AccountParams < StrongParameters::Core::ApplicationParams
31
+ # allow :name
32
+ # metadata :ip_address, :role
33
+ # end
34
+ #
35
+ # @example With transformations
36
+ # class UserParams < StrongParameters::Core::ApplicationParams
37
+ # allow :email
38
+ # allow :role
39
+ # metadata :current_user
40
+ #
41
+ # transform :email do |value, metadata|
42
+ # value&.downcase&.strip
43
+ # end
44
+ #
45
+ # transform :role do |value, metadata|
46
+ # metadata[:current_user]&.admin? ? value : 'user'
47
+ # end
48
+ # end
49
+ class ApplicationParams
50
+ class << self
51
+ # Returns the list of allowed attributes.
52
+ #
53
+ # @return [Array<Symbol>] array of allowed attribute names
54
+ def allowed_attributes
55
+ @allowed_attributes ||= []
56
+ end
57
+
58
+ # Returns the list of denied attributes.
59
+ #
60
+ # @return [Array<Symbol>] array of denied attribute names
61
+ def denied_attributes
62
+ @denied_attributes ||= []
63
+ end
64
+
65
+ # Returns the flags hash.
66
+ #
67
+ # @return [Hash<Symbol, Object>] hash of flag names to values
68
+ def flags
69
+ @flags ||= {}
70
+ end
71
+
72
+ # Returns the set of allowed metadata keys.
73
+ #
74
+ # @return [Set<Symbol>] set of allowed metadata key names
75
+ # @note :current_user is always implicitly allowed
76
+ def allowed_metadata
77
+ @allowed_metadata ||= Set.new
78
+ end
79
+
80
+ # Returns the hash of transformations.
81
+ #
82
+ # @return [Hash<Symbol, Proc>] hash of attribute names to transformation procs
83
+ def transformations
84
+ @transformations ||= {}
85
+ end
86
+
87
+ # DSL method to allow an attribute
88
+ # @param attribute [Symbol, String, Hash] the attribute name to allow, or a hash for arrays
89
+ # @param options [Hash] additional options
90
+ # - :only - only allow this attribute for these actions
91
+ # - :except - allow this attribute except for these actions
92
+ # - :array - if true, permit an array of scalar values
93
+ # Examples:
94
+ # allow :name # permits scalar name
95
+ # allow :tags, array: true # permits array of scalars
96
+ # allow :tags, only: :create # only for create action
97
+ def allow(attribute, options = {})
98
+ attribute = attribute.to_sym
99
+ allowed_attributes << attribute unless allowed_attributes.include?(attribute)
100
+
101
+ # Clear cache since permitted attributes may have changed
102
+ @permitted_cache = {} if instance_variable_defined?(:@permitted_cache)
103
+
104
+ # Store any additional options for this attribute
105
+ if options.any?
106
+ @attribute_options ||= {}
107
+ # Normalize :only and :except to arrays for consistency
108
+ normalized_options = options.dup
109
+ [:only, :except].each do |key|
110
+ if normalized_options[key] && !normalized_options[key].is_a?(Array)
111
+ normalized_options[key] = [normalized_options[key]]
112
+ end
113
+ end
114
+ @attribute_options[attribute] = normalized_options
115
+ end
116
+ end
117
+
118
+ # DSL method to deny an attribute
119
+ # @param attribute [Symbol, String] the attribute name to deny
120
+ def deny(attribute)
121
+ attribute = attribute.to_sym
122
+ denied_attributes << attribute unless denied_attributes.include?(attribute)
123
+ # Clear cache since permitted attributes may have changed
124
+ @permitted_cache = {} if instance_variable_defined?(:@permitted_cache)
125
+ end
126
+
127
+ # DSL method to set a flag
128
+ # @param name [Symbol, String] the flag name
129
+ # @param value [Boolean, Object] the flag value
130
+ def flag(name, value = true)
131
+ flags[name.to_sym] = value
132
+ end
133
+
134
+ # DSL method to declare allowed metadata keys
135
+ # @param key [Symbol, String] the metadata key to allow
136
+ # Note: :current_user is always allowed and doesn't need to be declared
137
+ def metadata(*keys)
138
+ keys.each do |key|
139
+ allowed_metadata << key.to_sym
140
+ end
141
+ end
142
+
143
+ # DSL method to define a transformation for an attribute
144
+ # @param attribute [Symbol, String] the attribute name to transform
145
+ # @param block [Proc] the transformation block
146
+ # The block receives two parameters:
147
+ # - value: the current value of the attribute
148
+ # - metadata: hash of metadata (current_user, action, etc.)
149
+ # The block should return the transformed value
150
+ # @example Transform email to lowercase
151
+ # transform :email do |value, metadata|
152
+ # value&.downcase&.strip
153
+ # end
154
+ # @example Conditional transformation based on metadata
155
+ # transform :role do |value, metadata|
156
+ # metadata[:current_user]&.admin? ? value : 'user'
157
+ # end
158
+ def transform(attribute, &block)
159
+ raise ArgumentError, "Block required for transform" unless block_given?
160
+ transformations[attribute.to_sym] = block
161
+ end
162
+
163
+ # Check if an attribute is allowed.
164
+ #
165
+ # An attribute is allowed if it's in the allowed list and not in the denied list.
166
+ # Uses memoization for better performance on repeated checks.
167
+ #
168
+ # @param attribute [Symbol, String] the attribute name
169
+ # @return [Boolean] true if allowed, false otherwise
170
+ def allowed?(attribute)
171
+ return false unless attribute.respond_to?(:to_sym)
172
+ attribute = attribute.to_sym
173
+ return false if denied_attributes.include?(attribute)
174
+
175
+ allowed_attributes.include?(attribute)
176
+ end
177
+
178
+ # Check if an attribute is denied.
179
+ #
180
+ # @param attribute [Symbol, String] the attribute name
181
+ # @return [Boolean] true if denied, false otherwise
182
+ def denied?(attribute)
183
+ return false unless attribute.respond_to?(:to_sym)
184
+ denied_attributes.include?(attribute.to_sym)
185
+ end
186
+
187
+ # Check if a flag is set
188
+ # @param name [Symbol, String] the flag name
189
+ # @return [Boolean, Object] the flag value
190
+ def flag?(name)
191
+ return nil unless name.respond_to?(:to_sym)
192
+ flags[name.to_sym]
193
+ end
194
+
195
+ # Check if a metadata key is allowed
196
+ # @param key [Symbol, String] the metadata key
197
+ # @return [Boolean] true if allowed, false otherwise
198
+ # Note: :current_user is always allowed
199
+ def metadata_allowed?(key)
200
+ return false unless key.respond_to?(:to_sym)
201
+ key = key.to_sym
202
+ key == :current_user || allowed_metadata.include?(key)
203
+ end
204
+
205
+ # Get options for an attribute
206
+ # @param attribute [Symbol, String] the attribute name
207
+ # @return [Hash] the options hash
208
+ def attribute_options(attribute)
209
+ @attribute_options ||= {}
210
+ return {} unless attribute.respond_to?(:to_sym)
211
+ @attribute_options[attribute.to_sym] || {}
212
+ end
213
+
214
+ # Generate a permit array suitable for strong_parameters.
215
+ #
216
+ # Returns an array of permitted attributes, optionally filtered by action.
217
+ # Results are cached per action for better performance.
218
+ #
219
+ # @param action [Symbol, String, nil] optional action name to filter by
220
+ # @return [Array<Symbol, Hash>] array of permitted attributes
221
+ # Returns symbols for scalar attributes, and {attr: []} for array attributes
222
+ # @example
223
+ # permitted_attributes # => [:name, :email]
224
+ # permitted_attributes(action: :create) # => [:name, :email, :published]
225
+ def permitted_attributes(action: nil)
226
+ # Use cache for performance on repeated calls
227
+ @permitted_cache ||= {}
228
+ cache_key = action || :__no_action__
229
+
230
+ return @permitted_cache[cache_key] if @permitted_cache.key?(cache_key)
231
+
232
+ attrs = allowed_attributes.dup
233
+
234
+ # Remove denied attributes
235
+ attrs.reject! { |attr| denied_attributes.include?(attr) }
236
+
237
+ # Filter by action-specific flags if provided
238
+ if action
239
+ action = action.to_sym
240
+ end
241
+ attrs.select! do |attr|
242
+ opts = attribute_options(attr)
243
+ if opts[:only]
244
+ action.nil? || Array(opts[:only]).include?(action)
245
+ elsif opts[:except]
246
+ action.nil? || !Array(opts[:except]).include?(action)
247
+ else
248
+ true
249
+ end
250
+ end
251
+
252
+ # Convert to proper permit format
253
+ # For array attributes, return {attr: []}, otherwise just the symbol
254
+ result = attrs.map do |attr|
255
+ opts = attribute_options(attr)
256
+ if opts[:array]
257
+ { attr => [] }
258
+ else
259
+ attr
260
+ end
261
+ end.freeze
262
+
263
+ @permitted_cache[cache_key] = result
264
+ end
265
+
266
+ # Apply transformations to a hash of parameters
267
+ #
268
+ # @param params [Hash, Parameters] the parameters to transform
269
+ # @param metadata [Hash] metadata hash containing current_user, action, etc.
270
+ # @return [Hash] the transformed parameters
271
+ def apply_transformations(params, metadata = {})
272
+ return params if transformations.empty?
273
+
274
+ # Handle non-hash params
275
+ return params unless params.is_a?(Hash) || params.respond_to?(:to_unsafe_h) || params.respond_to?(:to_h)
276
+
277
+ # Convert to regular hash - handle both plain hashes and Parameters objects
278
+ hash = if params.respond_to?(:to_unsafe_h)
279
+ # Rails ActionController::Parameters
280
+ params.to_unsafe_h
281
+ elsif params.respond_to?(:to_h)
282
+ # Core Parameters or plain Hash
283
+ params.to_h
284
+ else
285
+ params
286
+ end
287
+
288
+ # Make a dup so we don't modify the original
289
+ hash = hash.dup
290
+
291
+ # Apply each transformation
292
+ transformations.each do |attribute, transformer|
293
+ key_str = attribute.to_s
294
+ if hash.key?(key_str)
295
+ # Deep dup the value to prevent transformations from modifying the original
296
+ original_value = hash[key_str]
297
+ duped_value = begin
298
+ Marshal.load(Marshal.dump(original_value))
299
+ rescue TypeError
300
+ # If Marshal fails (e.g., due to procs or unserializable objects), shallow dup
301
+ original_value.dup
302
+ end
303
+ hash[key_str] = transformer.call(duped_value, metadata)
304
+ end
305
+ end
306
+
307
+ hash
308
+ end
309
+
310
+ # Inherit attributes from parent class.
311
+ #
312
+ # When a subclass is created, it inherits all configuration from the parent
313
+ # including allowed/denied attributes, flags, metadata, transformations, and options.
314
+ # This enables building specialized params classes on top of base ones.
315
+ #
316
+ # @param subclass [Class] the inheriting subclass
317
+ # @return [void]
318
+ def inherited(subclass)
319
+ super
320
+ # Copy parent's configuration to subclass
321
+ subclass.instance_variable_set(:@allowed_attributes, allowed_attributes.dup)
322
+ subclass.instance_variable_set(:@denied_attributes, denied_attributes.dup)
323
+ subclass.instance_variable_set(:@flags, flags.dup)
324
+ subclass.instance_variable_set(:@allowed_metadata, allowed_metadata.dup)
325
+ subclass.instance_variable_set(:@transformations, transformations.dup)
326
+ if instance_variable_defined?(:@attribute_options)
327
+ subclass.instance_variable_set(:@attribute_options, @attribute_options.dup)
328
+ end
329
+ # Don't copy the cache - let subclass build its own
330
+ end
331
+ end
332
+ end
333
+ end
334
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StrongParameters
4
+ module Core
5
+ # Configuration module for StrongParameters behavior.
6
+ #
7
+ # This module provides centralized configuration for how strong parameters
8
+ # behaves throughout the application, including handling of unpermitted
9
+ # parameters and notification mechanisms.
10
+ #
11
+ # @example Configure action on unpermitted parameters
12
+ # StrongParameters::Core::Configuration.action_on_unpermitted_parameters = :log
13
+ #
14
+ # @example Set custom notification handler
15
+ # StrongParameters::Core::Configuration.unpermitted_notification_handler = ->(keys) do
16
+ # Rails.logger.warn("Unpermitted parameters: #{keys.join(', ')}")
17
+ # end
18
+ module Configuration
19
+ class << self
20
+ # Action to take when unpermitted parameters are detected.
21
+ #
22
+ # **Options:**
23
+ # - `:log` - Log unpermitted parameters via notification handler
24
+ # - `:raise` - Raise UnpermittedParameters exception
25
+ # - `false` or `nil` - Ignore unpermitted parameters (not recommended for production)
26
+ #
27
+ # @return [Symbol, Boolean, nil] the configured action
28
+ #
29
+ # @example Enable logging
30
+ # Configuration.action_on_unpermitted_parameters = :log
31
+ #
32
+ # @example Enable strict mode (raise on unpermitted)
33
+ # Configuration.action_on_unpermitted_parameters = :raise
34
+ attr_accessor :action_on_unpermitted_parameters
35
+
36
+ # Handler for unpermitted parameter notifications.
37
+ #
38
+ # This proc/lambda is called when unpermitted parameters are detected
39
+ # and `action_on_unpermitted_parameters` is set to `:log`. The handler
40
+ # receives an array of unpermitted parameter keys.
41
+ #
42
+ # @return [Proc, nil] the notification handler
43
+ #
44
+ # @example Set custom handler
45
+ # Configuration.unpermitted_notification_handler = ->(keys) do
46
+ # MyLogger.warn("Unpermitted: #{keys.join(', ')}")
47
+ # end
48
+ attr_accessor :unpermitted_notification_handler
49
+
50
+ # Parameters that are never considered unpermitted.
51
+ #
52
+ # These are typically framework-added parameters that are not security
53
+ # concerns (like 'controller' and 'action' in Rails).
54
+ #
55
+ # @return [Array<String>] array of parameter names to always allow
56
+ attr_accessor :always_permitted_parameters
57
+
58
+ # Reset configuration to default values.
59
+ #
60
+ # @return [void]
61
+ def reset!
62
+ @action_on_unpermitted_parameters = nil
63
+ @unpermitted_notification_handler = nil
64
+ @always_permitted_parameters = %w[controller action].freeze
65
+ end
66
+
67
+ # Apply configuration to Parameters class.
68
+ #
69
+ # This method synchronizes the configuration module settings with the
70
+ # Parameters class class variables for backwards compatibility.
71
+ #
72
+ # @return [void]
73
+ def apply!
74
+ Parameters.action_on_unpermitted_parameters = action_on_unpermitted_parameters
75
+ Parameters.unpermitted_notification_handler = unpermitted_notification_handler
76
+ end
77
+ end
78
+
79
+ # Initialize with defaults
80
+ reset!
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StrongParameters
4
+ module Core
5
+ # Exception raised when attempting mass assignment with unpermitted parameters.
6
+ #
7
+ # This exception is raised when Parameters are used in mass assignment
8
+ # without being explicitly permitted using permit() or permit!().
9
+ #
10
+ # @example
11
+ # User.new(params[:user])
12
+ # # => StrongParameters::Core::ForbiddenAttributes (if :user params not permitted)
13
+ class ForbiddenAttributes < StandardError
14
+ end
15
+
16
+ # Protection module for mass assignment.
17
+ #
18
+ # This module can be included in model classes to provide protection
19
+ # against unpermitted mass assignment.
20
+ #
21
+ # @example Include in a model
22
+ # class Post
23
+ # include StrongParameters::Core::ForbiddenAttributesProtection
24
+ #
25
+ # def initialize(attributes = {})
26
+ # assign_attributes(attributes)
27
+ # end
28
+ #
29
+ # def assign_attributes(attributes)
30
+ # attributes = sanitize_for_mass_assignment(attributes)
31
+ # # ... assign attributes
32
+ # end
33
+ # end
34
+ module ForbiddenAttributesProtection
35
+ # Check if parameters are permitted before mass assignment.
36
+ #
37
+ # @param attributes [Object] mass assignment attributes
38
+ # @return [Object] the attributes if permitted
39
+ # @raise [ForbiddenAttributes] if parameters are not permitted
40
+ def sanitize_for_mass_assignment(attributes)
41
+ if attributes.respond_to?(:permitted?) && !attributes.permitted?
42
+ raise ForbiddenAttributes
43
+ end
44
+ attributes
45
+ end
46
+ end
47
+ end
48
+ end