wallaby-core 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.
Files changed (108) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +31 -0
  4. data/app/controllers/wallaby/application_controller.rb +84 -0
  5. data/app/controllers/wallaby/resources_controller.rb +381 -0
  6. data/app/controllers/wallaby/secure_controller.rb +81 -0
  7. data/app/security/ability.rb +13 -0
  8. data/config/locales/wallaby.en.yml +140 -0
  9. data/config/locales/wallaby_class.en.yml +30 -0
  10. data/config/routes.rb +39 -0
  11. data/lib/adaptors/wallaby/custom.rb +7 -0
  12. data/lib/adaptors/wallaby/custom/default_provider.rb +9 -0
  13. data/lib/adaptors/wallaby/custom/model_decorator.rb +71 -0
  14. data/lib/adaptors/wallaby/custom/model_finder.rb +13 -0
  15. data/lib/adaptors/wallaby/custom/model_pagination_provider.rb +14 -0
  16. data/lib/adaptors/wallaby/custom/model_service_provider.rb +48 -0
  17. data/lib/authorizers/wallaby/cancancan_authorization_provider.rb +72 -0
  18. data/lib/authorizers/wallaby/default_authorization_provider.rb +58 -0
  19. data/lib/authorizers/wallaby/model_authorizer.rb +100 -0
  20. data/lib/authorizers/wallaby/pundit_authorization_provider.rb +89 -0
  21. data/lib/concerns/wallaby/authorizable.rb +103 -0
  22. data/lib/concerns/wallaby/baseable.rb +36 -0
  23. data/lib/concerns/wallaby/decoratable.rb +101 -0
  24. data/lib/concerns/wallaby/defaultable.rb +38 -0
  25. data/lib/concerns/wallaby/engineable.rb +61 -0
  26. data/lib/concerns/wallaby/fieldable.rb +78 -0
  27. data/lib/concerns/wallaby/paginatable.rb +72 -0
  28. data/lib/concerns/wallaby/rails_overridden_methods.rb +42 -0
  29. data/lib/concerns/wallaby/resourcable.rb +149 -0
  30. data/lib/concerns/wallaby/servicable.rb +68 -0
  31. data/lib/concerns/wallaby/shared_helpers.rb +22 -0
  32. data/lib/concerns/wallaby/themeable.rb +40 -0
  33. data/lib/decorators/wallaby/resource_decorator.rb +189 -0
  34. data/lib/errors/wallaby/cell_handling.rb +6 -0
  35. data/lib/errors/wallaby/forbidden.rb +6 -0
  36. data/lib/errors/wallaby/general_error.rb +6 -0
  37. data/lib/errors/wallaby/invalid_error.rb +6 -0
  38. data/lib/errors/wallaby/model_not_found.rb +11 -0
  39. data/lib/errors/wallaby/not_authenticated.rb +6 -0
  40. data/lib/errors/wallaby/not_found.rb +6 -0
  41. data/lib/errors/wallaby/not_implemented.rb +6 -0
  42. data/lib/errors/wallaby/resource_not_found.rb +11 -0
  43. data/lib/errors/wallaby/unprocessable_entity.rb +6 -0
  44. data/lib/forms/wallaby/form_builder.rb +60 -0
  45. data/lib/helpers/wallaby/application_helper.rb +79 -0
  46. data/lib/helpers/wallaby/base_helper.rb +65 -0
  47. data/lib/helpers/wallaby/configuration_helper.rb +18 -0
  48. data/lib/helpers/wallaby/form_helper.rb +62 -0
  49. data/lib/helpers/wallaby/index_helper.rb +84 -0
  50. data/lib/helpers/wallaby/links_helper.rb +213 -0
  51. data/lib/helpers/wallaby/resources_helper.rb +52 -0
  52. data/lib/helpers/wallaby/secure_helper.rb +54 -0
  53. data/lib/helpers/wallaby/styling_helper.rb +82 -0
  54. data/lib/interfaces/wallaby/mode.rb +72 -0
  55. data/lib/interfaces/wallaby/model_authorization_provider.rb +99 -0
  56. data/lib/interfaces/wallaby/model_decorator.rb +168 -0
  57. data/lib/interfaces/wallaby/model_finder.rb +12 -0
  58. data/lib/interfaces/wallaby/model_pagination_provider.rb +107 -0
  59. data/lib/interfaces/wallaby/model_service_provider.rb +84 -0
  60. data/lib/paginators/wallaby/model_paginator.rb +115 -0
  61. data/lib/paginators/wallaby/resource_paginator.rb +12 -0
  62. data/lib/parsers/wallaby/parser.rb +34 -0
  63. data/lib/renderers/wallaby/cell.rb +137 -0
  64. data/lib/renderers/wallaby/cell_resolver.rb +89 -0
  65. data/lib/renderers/wallaby/custom_lookup_context.rb +64 -0
  66. data/lib/renderers/wallaby/custom_partial_renderer.rb +33 -0
  67. data/lib/renderers/wallaby/custom_renderer.rb +16 -0
  68. data/lib/responders/wallaby/json_api_responder.rb +101 -0
  69. data/lib/responders/wallaby/resources_responder.rb +28 -0
  70. data/lib/routes/wallaby/resources_router.rb +72 -0
  71. data/lib/servicers/wallaby/model_servicer.rb +154 -0
  72. data/lib/services/wallaby/engine_name_finder.rb +22 -0
  73. data/lib/services/wallaby/engine_url_for.rb +46 -0
  74. data/lib/services/wallaby/link_options_normalizer.rb +19 -0
  75. data/lib/services/wallaby/map/mode_mapper.rb +27 -0
  76. data/lib/services/wallaby/map/model_class_collector.rb +49 -0
  77. data/lib/services/wallaby/map/model_class_mapper.rb +38 -0
  78. data/lib/services/wallaby/prefixes_builder.rb +66 -0
  79. data/lib/services/wallaby/sorting/hash_builder.rb +19 -0
  80. data/lib/services/wallaby/sorting/link_builder.rb +69 -0
  81. data/lib/services/wallaby/sorting/next_builder.rb +63 -0
  82. data/lib/services/wallaby/sorting/single_builder.rb +20 -0
  83. data/lib/services/wallaby/type_renderer.rb +50 -0
  84. data/lib/support/action_dispatch/routing/mapper.rb +75 -0
  85. data/lib/tree/wallaby/node.rb +25 -0
  86. data/lib/utils/wallaby/cell_utils.rb +34 -0
  87. data/lib/utils/wallaby/field_utils.rb +43 -0
  88. data/lib/utils/wallaby/filter_utils.rb +20 -0
  89. data/lib/utils/wallaby/model_utils.rb +51 -0
  90. data/lib/utils/wallaby/module_utils.rb +46 -0
  91. data/lib/utils/wallaby/params_utils.rb +14 -0
  92. data/lib/utils/wallaby/preload_utils.rb +44 -0
  93. data/lib/utils/wallaby/test_utils.rb +34 -0
  94. data/lib/utils/wallaby/utils.rb +27 -0
  95. data/lib/wallaby/configuration.rb +103 -0
  96. data/lib/wallaby/configuration/features.rb +24 -0
  97. data/lib/wallaby/configuration/mapping.rb +140 -0
  98. data/lib/wallaby/configuration/metadata.rb +23 -0
  99. data/lib/wallaby/configuration/models.rb +46 -0
  100. data/lib/wallaby/configuration/pagination.rb +30 -0
  101. data/lib/wallaby/configuration/security.rb +98 -0
  102. data/lib/wallaby/configuration/sorting.rb +28 -0
  103. data/lib/wallaby/constants.rb +45 -0
  104. data/lib/wallaby/core.rb +117 -0
  105. data/lib/wallaby/core/version.rb +7 -0
  106. data/lib/wallaby/engine.rb +43 -0
  107. data/lib/wallaby/map.rb +170 -0
  108. metadata +222 -0
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # Type renderer
5
+ class TypeRenderer
6
+ class << self
7
+ # Render partial
8
+ # @param view [ActionView]
9
+ # @param options [Hash]
10
+ # @param locals [Hash]
11
+ # @return [String] HTML
12
+ def render(view, options = {}, locals = {}, &block)
13
+ locals[:object] ||= locals[:form].try :object
14
+ check locals
15
+ complete locals, view.params[:action]
16
+ view.render options, locals, &block
17
+ end
18
+
19
+ private
20
+
21
+ # @param locals [Hash]
22
+ # @raise [ArgumentError] if form is set but blank
23
+ # @raise [ArgumentError] if field_name is not provided
24
+ # @raise [ArgumentError] if object is not decorated
25
+ def check(locals)
26
+ raise ArgumentError, I18n.t('errors.required', subject: 'form') if locals.key?(:form) && locals[:form].blank?
27
+ raise ArgumentError, I18n.t('errors.required', subject: 'field_name') if locals[:field_name].blank?
28
+ raise ArgumentError, 'Object is not decorated.' unless locals[:object].is_a? ResourceDecorator
29
+ end
30
+
31
+ # @param locals [Hash]
32
+ # @param action [String]
33
+ def complete(locals, action)
34
+ action_name = CellUtils.to_action_prefix action
35
+ locals[:metadata] = locals[:object].public_send :"#{action_name}_metadata_of", locals[:field_name]
36
+ locals[:value] = locals[:object].public_send locals[:field_name]
37
+ end
38
+
39
+ # @param options [String]
40
+ # @param view [ActionView]
41
+ # @return [String] partial path string
42
+ # @return [String] blank string
43
+ def find_partial(options, view)
44
+ formats = [view.request.format.to_sym]
45
+ lookup = view.lookup_context
46
+ lookup.find_template options, lookup.prefixes, true, [], formats: formats
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionDispatch
4
+ module Routing
5
+ # Re-open `ActionDispatch::Routing::Mapper` to add route helpers for Wallaby.
6
+ class Mapper
7
+ # Generate **resourceful** routes that works for Wallaby.
8
+ # @example To generate resourceful routes that works for Wallaby:
9
+ # wresources :postcodes
10
+ # # => same effect as
11
+ # resources(
12
+ # :postcodes,
13
+ # path: ':resources',
14
+ # defaults: { resources: :postcodes },
15
+ # constraints: { resources: :postcodes }
16
+ # )
17
+ # @param resource_names [Array<String, Symbol>]
18
+ # @see https://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Resources.html#method-i-resources
19
+ # ActionDispatch::Routing::Mapper::Resources#resources
20
+ def wresources(*resource_names, &block)
21
+ options = resource_names.extract_options!.dup
22
+ resource_names.each do |resource_name|
23
+ new_options = wallaby_resources_options_for resource_name, options
24
+ resources resource_name, new_options, &block
25
+ end
26
+ end
27
+
28
+ # Generate **resourceful** routes that works for Wallaby.
29
+ # @example To generate resourceful routes that works for Wallaby:
30
+ # wresource :profile
31
+ # # => same effect as
32
+ # resource(
33
+ # :profile,
34
+ # path: ':resource',
35
+ # defaults: { resource: :profile, resources: :profiles },
36
+ # constraints: { resource: :profile, resources: :profiles }
37
+ # )
38
+ # @param resource_names [Array<String, Symbol>]
39
+ # @see https://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Resources.html#method-i-resource
40
+ # ActionDispatch::Routing::Mapper::Resources#resource
41
+ def wresource(*resource_names, &block)
42
+ options = resource_names.extract_options!.dup
43
+ resource_names.each do |resource_name|
44
+ new_options = wallaby_resource_options_for resource_name, options
45
+ resource resource_name, new_options, &block
46
+ end
47
+ end
48
+
49
+ protected
50
+
51
+ # Fill in the **resources** options required by Wallaby
52
+ # @param resources_name [String, Symbol]
53
+ # @param options [Hash]
54
+ def wallaby_resources_options_for(resources_name, options)
55
+ { path: ':resources' }.merge!(options).tap do |new_options|
56
+ %i(defaults constraints).each do |key|
57
+ new_options[key] = { resources: resources_name }.merge!(new_options[key] || {})
58
+ end
59
+ end
60
+ end
61
+
62
+ # Fill in the **resource** options required by Wallaby
63
+ # @param resource_name [String, Symbol]
64
+ # @param options [Hash]
65
+ def wallaby_resource_options_for(resource_name, options)
66
+ plural_resources = Wallaby::ModelUtils.to_resources_name resource_name
67
+ { path: ':resource' }.merge!(options).tap do |new_options|
68
+ %i(defaults constraints).each do |key|
69
+ new_options[key] = { resource: resource_name, resources: plural_resources }.merge!(new_options[key] || {})
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # To present classes in tree structure.
5
+ class Node
6
+ # @!attribute [r] klass
7
+ # Represent the current class
8
+ attr_reader :klass
9
+ # @!attribute parent
10
+ # Represent the parent class of current class
11
+ attr_accessor :parent
12
+
13
+ delegate :name, to: :klass
14
+
15
+ # @param klass [Class]
16
+ def initialize(klass)
17
+ @klass = klass
18
+ end
19
+
20
+ # @return [Array<Class>] a list of children classes
21
+ def children
22
+ @children ||= []
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # Cell utils
5
+ module CellUtils
6
+ class << self
7
+ # Render a cell and produce output
8
+ # @param context [ActionView::Context]
9
+ # @param file_name [String]
10
+ # @param locals [Hash]
11
+ # @return [String] output
12
+ def render(context, file_name, locals = {}, &block)
13
+ snake_class = file_name[%r{(?<=app/).+(?=\.rb)}].split(SLASH, 2).last
14
+ cell_class = snake_class.camelize.constantize
15
+ Rails.logger.info " Rendered [cell] #{file_name}"
16
+ cell_class.new(context, locals).render_complete(&block)
17
+ end
18
+
19
+ # Check if a partial is a cell or not
20
+ # @param partial_path [String]
21
+ # @return [true] if partial is a `rb` file
22
+ # @return [false] otherwise
23
+ def find_cell(*partials)
24
+ partials.find { |partial| partial.end_with? '.rb' }
25
+ end
26
+
27
+ # @param action_name [String, Symbol]
28
+ # @return [String, Symbol] action prefix
29
+ def to_action_prefix(action_name)
30
+ FORM_ACTIONS[action_name] || action_name
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # Field utils
5
+ module FieldUtils
6
+ class << self
7
+ # Find the first field that meets the first condition.
8
+ # @example to find the possible text field
9
+ # Wallaby::FieldUtils.first_field_by({ name: /name|title/ }, { type: 'string' }, fields)
10
+ # # => if any field name that has `name` or `title`, return this field
11
+ # # => otherwise, find the first field that has type `string`
12
+ # @param conditions [Array<Hash>]
13
+ # @param fields [Hash] field metadata
14
+ # @return [String, Symbol] field name
15
+ def first_field_by(*conditions, fields)
16
+ return if [conditions, fields].any?(&:blank?)
17
+
18
+ conditions.each do |condition|
19
+ fields.each do |field_name, metadata|
20
+ return field_name if meet? field_name, metadata.with_indifferent_access, condition
21
+ end
22
+ end
23
+ nil
24
+ end
25
+
26
+ protected
27
+
28
+ # @param field_name [String]
29
+ # @param metadata [Hash]
30
+ # @param condition [Hash]
31
+ # @return [true] if field's metadata meets the condition
32
+ # @return [true] otherwise
33
+ def meet?(field_name, metadata, condition)
34
+ condition.all? do |key, requirement|
35
+ operator = requirement.is_a?(::Regexp) ? '=~' : '=='
36
+ value = metadata[key]
37
+ value ||= field_name.to_s if key.to_sym == :name
38
+ ModuleUtils.try_to value, operator, requirement
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # Filter utils
5
+ module FilterUtils
6
+ # Find filter name in the following precedences from high to low:
7
+ #
8
+ # - `filter_name` argument
9
+ # - filters that has been marked as default
10
+ # - `:all`
11
+ # @param filter_name [String, Symbol] filter name
12
+ # @param filters [Hash] filter metadata
13
+ # @return [String, Symbol]
14
+ def self.filter_name_by(filter_name, filters)
15
+ filter = filter_name # from param
16
+ filter ||= filters.find { |_k, v| v[:default] }.try(:first) # from default value
17
+ filter || :all # last resort
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # Utils for model
5
+ module ModelUtils
6
+ class << self
7
+ # Convert model class (e.g. `Namespace::Product`) into resources name (e.g. `namespace::products`)
8
+ # @param model_class [Class, String] model class
9
+ # @return [String] resources name
10
+ def to_resources_name(model_class)
11
+ return EMPTY_STRING if model_class.blank?
12
+
13
+ model_class.to_s.underscore.gsub(SLASH, COLONS).pluralize
14
+ end
15
+
16
+ # Produce model label (e.g. `Namespace / Product`) for model class (e.g. `Namespace::Product`)
17
+ # @param model_class [Class, String] model class
18
+ # @return [String] model label
19
+ def to_model_label(model_class)
20
+ # TODO: change to use i18n translation
21
+ return EMPTY_STRING if model_class.blank?
22
+
23
+ model_class_name = to_model_name model_class
24
+ model_class_name.titleize.gsub(SLASH, SPACE + SLASH + SPACE)
25
+ end
26
+
27
+ # Convert resources name (e.g. `namespace::products`) into model class (e.g. `Namespace::Product`)
28
+ # @param resources_name [String] resources name
29
+ # @return [Class] model class
30
+ # @return [nil] when not found
31
+ def to_model_class(resources_name)
32
+ return if resources_name.blank?
33
+
34
+ class_name = to_model_name resources_name
35
+ class_name.constantize
36
+ rescue NameError
37
+ Rails.logger.warn I18n.t('errors.not_found.model', model: class_name)
38
+ nil
39
+ end
40
+
41
+ # Convert resources name (e.g. `namespace::products`) into model name (e.g. `Namespace::Product`)
42
+ # @param resources_name [String] resources name
43
+ # @return [String] model name
44
+ def to_model_name(resources_name)
45
+ return EMPTY_STRING if resources_name.blank?
46
+
47
+ resources_name.to_s.singularize.gsub(COLONS, SLASH).camelize
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # Utils for module and class
5
+ module ModuleUtils
6
+ class << self
7
+ # A helper method to check if subject responds to given method and to return the result if so
8
+ # @param subject [Object]
9
+ # @param method_id [String, Symbol]
10
+ # @param args [Array] a list of arguments
11
+ # @return [Object] result from executing given method on subject
12
+ # @return [nil] if subject doesn't respond to given method
13
+ def try_to(subject, method_id, *args, &block)
14
+ return if method_id.blank?
15
+
16
+ subject.respond_to?(method_id) && subject.public_send(method_id, *args, &block) || nil
17
+ end
18
+
19
+ # Check whether a class is anonymous or not
20
+ # @param klass [Class]
21
+ # @return [true] if a class is anonymous
22
+ # @return [false] otherwise
23
+ def anonymous_class?(klass)
24
+ klass.name.blank? || klass.to_s.start_with?('#<Class')
25
+ end
26
+
27
+ # Check if a child class inherits from parent class
28
+ # @param child [Class] child class
29
+ # @param parent [Class] parent class
30
+ # @raise [ArgumentError] if given class is not a child of the other class
31
+ def inheritance_check(child, parent)
32
+ return unless child && parent
33
+ return if child < parent
34
+
35
+ raise ::ArgumentError, I18n.t('errors.invalid.inheritance', klass: child, parent: parent)
36
+ end
37
+
38
+ # If block is given, run the block. Otherwise, return subject
39
+ # @param subject [Object]
40
+ # @yield [subject]
41
+ def yield_for(subject)
42
+ block_given? ? yield(subject) : subject
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # Hash utils
5
+ module ParamsUtils
6
+ class << self
7
+ # @param params [Array<Hash>]
8
+ # @return [Hash] combined hash that removes empty values
9
+ def presence(*params)
10
+ params.reduce({}, :merge).delete_if { |_, v| v.nil? || v == '' }
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # Preload utils
5
+ module PreloadUtils
6
+ class << self
7
+ # Preload all files under app folder
8
+ def require_all
9
+ eager_load_paths.map(&method(:require_one))
10
+ end
11
+
12
+ # Require files under a load path
13
+ # @param load_path [String, Pathname]
14
+ # @see https://api.rubyonrails.org/classes/Rails/Engine.html#method-i-eager_load-21 Rails::Engine#eager_load!
15
+ def require_one(load_path)
16
+ Dir.glob("#{load_path}/**/*.rb").sort.each(&method(:load_class_for))
17
+ end
18
+
19
+ protected
20
+
21
+ # @return [Array<String, Pathname>] a list of sorted eager load paths which lists `app/models`
22
+ # at highest precedence
23
+ def eager_load_paths
24
+ Rails.configuration.eager_load_paths.sort_by do |path|
25
+ - path.index(%r{/models$}).to_i
26
+ end
27
+ end
28
+
29
+ # `constantize` is used to make Rails to handle all sort of load errors
30
+ #
31
+ # NOTE: don't try to use `ActiveSupport::Dependencies::Loadable.require_dependency`.
32
+ # As `require_dependency` does not take care all errors raised when class/module is loaded.
33
+ # @param file_path [Pathname, String]
34
+ def load_class_for(file_path)
35
+ module_name = file_path[%r{app/[^/]+/(.+)\.rb}, 1].gsub('/concerns/', '/')
36
+ class_name = module_name.camelize
37
+ class_name.constantize unless Module.const_defined? class_name
38
+ rescue NameError, LoadError => e
39
+ Rails.logger.debug " [WALLABY] Preload warning: #{e.message} from #{file_path}"
40
+ Rails.logger.debug e.backtrace.slice(0, 5)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # Utils for test
5
+ module TestUtils
6
+ class << self
7
+ # @param context
8
+ # @param controller_class [Class]
9
+ def around_crud(context, controller_class = nil)
10
+ context.before do
11
+ controller_class ||= described_class
12
+ controller_path = controller_class.controller_path
13
+ Wallaby::TestUtils.draw(routes, controller_path)
14
+ end
15
+
16
+ context.after { Rails.application.reload_routes! }
17
+ end
18
+
19
+ # @param routes
20
+ # @param controller_path [String]
21
+ def draw(routes, controller_path)
22
+ routes.draw do
23
+ get ':resources', to: "#{controller_path}#index", as: :resources
24
+ get ':resources/:id', to: "#{controller_path}#show", as: :resource
25
+ get ':resources/new', to: "#{controller_path}#new"
26
+ get ':resources/:id/edit', to: "#{controller_path}#edit"
27
+ post ':resources', to: "#{controller_path}#create"
28
+ patch ':resources/:id', to: "#{controller_path}#update"
29
+ delete ':resources/:id', to: "#{controller_path}#destroy"
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end