wallaby-core 0.2.11 → 0.3.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (139) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/wallaby/resources_controller.rb +20 -7
  3. data/app/security/ability.rb +1 -1
  4. data/config/routes.rb +21 -14
  5. data/lib/adaptors/wallaby/custom/default_provider.rb +1 -1
  6. data/lib/adaptors/wallaby/custom/model_decorator.rb +5 -17
  7. data/lib/adaptors/wallaby/custom/model_finder.rb +3 -3
  8. data/lib/adaptors/wallaby/custom/model_pagination_provider.rb +1 -1
  9. data/lib/adaptors/wallaby/custom/model_service_provider.rb +1 -1
  10. data/lib/authorizers/wallaby/cancancan_authorization_provider.rb +12 -5
  11. data/lib/authorizers/wallaby/default_authorization_provider.rb +10 -1
  12. data/lib/authorizers/wallaby/model_authorizer.rb +41 -16
  13. data/lib/authorizers/wallaby/pundit_authorization_provider.rb +22 -8
  14. data/lib/concerns/wallaby/application_concern.rb +41 -71
  15. data/lib/concerns/wallaby/authentication_concern.rb +29 -127
  16. data/lib/concerns/wallaby/authorizable.rb +14 -57
  17. data/lib/concerns/wallaby/baseable.rb +24 -57
  18. data/lib/concerns/wallaby/configurable.rb +416 -0
  19. data/lib/concerns/wallaby/decoratable.rb +24 -60
  20. data/lib/concerns/wallaby/engineable.rb +29 -46
  21. data/lib/concerns/wallaby/fieldable.rb +45 -56
  22. data/lib/concerns/wallaby/paginatable.rb +20 -51
  23. data/lib/concerns/wallaby/prefixable.rb +24 -4
  24. data/lib/concerns/wallaby/resourcable.rb +130 -72
  25. data/lib/concerns/wallaby/resources_concern.rb +205 -305
  26. data/lib/concerns/wallaby/servicable.rb +8 -48
  27. data/lib/concerns/wallaby/urlable.rb +69 -0
  28. data/lib/decorators/wallaby/resource_decorator.rb +72 -34
  29. data/lib/errors/wallaby/class_not_found.rb +1 -2
  30. data/lib/errors/wallaby/forbidden.rb +1 -2
  31. data/lib/errors/wallaby/general_error.rb +1 -1
  32. data/lib/errors/wallaby/invalid_error.rb +1 -2
  33. data/lib/errors/wallaby/method_removed.rb +5 -0
  34. data/lib/errors/wallaby/model_not_found.rb +1 -2
  35. data/lib/errors/wallaby/not_authenticated.rb +1 -2
  36. data/lib/errors/wallaby/not_found.rb +1 -2
  37. data/lib/errors/wallaby/not_implemented.rb +1 -2
  38. data/lib/errors/wallaby/resource_not_found.rb +1 -2
  39. data/lib/errors/wallaby/unprocessable_entity.rb +1 -2
  40. data/lib/fields/wallaby/all_fields.rb +63 -0
  41. data/lib/forms/wallaby/form_builder.rb +2 -2
  42. data/lib/generators/wallaby/engine/application_generator.rb +33 -0
  43. data/lib/generators/wallaby/engine/authorizer/USAGE +20 -0
  44. data/lib/generators/wallaby/engine/authorizer/authorizer_generator.rb +19 -0
  45. data/lib/generators/wallaby/engine/authorizer/templates/authorizer.rb.erb +35 -0
  46. data/lib/generators/wallaby/engine/controller/USAGE +20 -0
  47. data/lib/generators/wallaby/engine/controller/controller_generator.rb +23 -0
  48. data/lib/generators/wallaby/engine/controller/templates/controller.rb.erb +130 -0
  49. data/lib/generators/wallaby/engine/decorator/USAGE +20 -0
  50. data/lib/generators/wallaby/engine/decorator/decorator_generator.rb +19 -0
  51. data/lib/generators/wallaby/engine/decorator/templates/decorator.rb.erb +5 -0
  52. data/lib/generators/wallaby/engine/install/USAGE +19 -0
  53. data/lib/generators/wallaby/engine/install/install_generator.rb +91 -0
  54. data/lib/generators/wallaby/engine/install/templates/application_authorizer.rb.erb +37 -0
  55. data/lib/generators/wallaby/engine/install/templates/application_controller.rb.erb +173 -0
  56. data/lib/generators/wallaby/engine/install/templates/application_decorator.rb.erb +7 -0
  57. data/lib/generators/wallaby/engine/install/templates/application_paginator.rb.erb +27 -0
  58. data/lib/generators/wallaby/engine/install/templates/application_servicer.rb.erb +47 -0
  59. data/lib/generators/wallaby/engine/install/templates/initializer.rb.erb +16 -0
  60. data/lib/generators/wallaby/engine/paginator/USAGE +20 -0
  61. data/lib/generators/wallaby/engine/paginator/paginator_generator.rb +19 -0
  62. data/lib/generators/wallaby/engine/paginator/templates/paginator.rb.erb +25 -0
  63. data/lib/generators/wallaby/engine/servicer/USAGE +20 -0
  64. data/lib/generators/wallaby/engine/servicer/servicer_generator.rb +19 -0
  65. data/lib/generators/wallaby/engine/servicer/templates/servicer.rb.erb +45 -0
  66. data/lib/helpers/wallaby/application_helper.rb +10 -59
  67. data/lib/helpers/wallaby/base_helper.rb +11 -11
  68. data/lib/helpers/wallaby/configuration_helper.rb +36 -4
  69. data/lib/helpers/wallaby/form_helper.rb +1 -1
  70. data/lib/helpers/wallaby/index_helper.rb +19 -9
  71. data/lib/helpers/wallaby/links_helper.rb +13 -80
  72. data/lib/helpers/wallaby/resources_helper.rb +39 -7
  73. data/lib/helpers/wallaby/secure_helper.rb +20 -19
  74. data/lib/interfaces/wallaby/mode.rb +8 -8
  75. data/lib/interfaces/wallaby/model_authorization_provider.rb +23 -22
  76. data/lib/interfaces/wallaby/model_decorator.rb +36 -48
  77. data/lib/interfaces/wallaby/model_finder.rb +3 -3
  78. data/lib/interfaces/wallaby/model_pagination_provider.rb +2 -6
  79. data/lib/interfaces/wallaby/model_service_provider.rb +4 -4
  80. data/lib/paginators/wallaby/model_paginator.rb +1 -1
  81. data/lib/responders/wallaby/json_api_responder.rb +10 -5
  82. data/lib/responders/wallaby/resources_responder.rb +7 -2
  83. data/lib/routes/wallaby/engines/base_route.rb +78 -0
  84. data/lib/routes/wallaby/engines/custom_app_route.rb +92 -0
  85. data/lib/routes/wallaby/engines/engine_route.rb +77 -0
  86. data/lib/routes/wallaby/resources_router.rb +100 -45
  87. data/lib/servicers/wallaby/model_servicer.rb +13 -13
  88. data/lib/services/wallaby/authorizer_finder.rb +23 -0
  89. data/lib/services/wallaby/class_finder.rb +42 -0
  90. data/lib/services/wallaby/controller_finder.rb +29 -0
  91. data/lib/services/wallaby/decorator_finder.rb +34 -0
  92. data/lib/services/wallaby/default_models_excluder.rb +45 -0
  93. data/lib/services/wallaby/engine_name_finder.rb +14 -11
  94. data/lib/services/wallaby/engine_url_for.rb +82 -37
  95. data/lib/services/wallaby/fields_regulator.rb +34 -0
  96. data/lib/services/wallaby/map/mode_mapper.rb +4 -4
  97. data/lib/services/wallaby/map/model_class_mapper.rb +1 -1
  98. data/lib/services/wallaby/model_class_filter.rb +29 -0
  99. data/lib/services/wallaby/paginator_finder.rb +24 -0
  100. data/lib/services/wallaby/prefixes_builder.rb +49 -8
  101. data/lib/services/wallaby/servicer_finder.rb +31 -0
  102. data/lib/services/wallaby/sorting/hash_builder.rb +9 -0
  103. data/lib/services/wallaby/sorting/link_builder.rb +7 -10
  104. data/lib/services/wallaby/sorting/next_builder.rb +1 -12
  105. data/lib/services/wallaby/sorting/single_builder.rb +1 -1
  106. data/lib/support/action_dispatch/routing/mapper.rb +29 -4
  107. data/lib/utils/wallaby/field_utils.rb +9 -8
  108. data/lib/utils/wallaby/inflector.rb +94 -0
  109. data/lib/utils/wallaby/locale.rb +2 -2
  110. data/lib/utils/wallaby/module_utils.rb +3 -10
  111. data/lib/utils/wallaby/utils.rb +21 -14
  112. data/lib/wallaby/class_array.rb +18 -13
  113. data/lib/wallaby/class_hash.rb +16 -14
  114. data/lib/wallaby/classifier.rb +4 -2
  115. data/lib/wallaby/configuration/features.rb +8 -2
  116. data/lib/wallaby/configuration/mapping.rb +66 -112
  117. data/lib/wallaby/configuration/metadata.rb +15 -12
  118. data/lib/wallaby/configuration/models.rb +27 -25
  119. data/lib/wallaby/configuration/pagination.rb +15 -19
  120. data/lib/wallaby/configuration/security.rb +88 -80
  121. data/lib/wallaby/configuration/sorting.rb +15 -17
  122. data/lib/wallaby/configuration.rb +58 -23
  123. data/lib/wallaby/constants.rb +21 -13
  124. data/lib/wallaby/core/version.rb +1 -1
  125. data/lib/wallaby/core.rb +34 -10
  126. data/lib/wallaby/deprecator.rb +81 -0
  127. data/lib/wallaby/engine.rb +2 -19
  128. data/lib/wallaby/guesser.rb +45 -0
  129. data/lib/wallaby/logger.rb +35 -13
  130. data/lib/wallaby/map.rb +11 -88
  131. data/lib/wallaby/preloader.rb +9 -31
  132. metadata +120 -15
  133. data/config/locales/wallaby_class.en.yml +0 -9
  134. data/lib/concerns/wallaby/defaultable.rb +0 -38
  135. data/lib/concerns/wallaby/shared_helpers.rb +0 -22
  136. data/lib/services/wallaby/map/model_class_collector.rb +0 -49
  137. data/lib/services/wallaby/type_renderer.rb +0 -40
  138. data/lib/utils/wallaby/model_utils.rb +0 -52
  139. data/lib/utils/wallaby/test_utils.rb +0 -34
@@ -1,46 +1,91 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wallaby
4
- # {Wallaby::ApplicationHelper#url_for} helper for Wallaby engine
4
+ # URL service object for {Urlable#url_for} helper
5
+ #
6
+ # Since {Engine}'s {https://github.com/wallaby-rails/wallaby-core/blob/master/config/routes.rb routes}
7
+ # are declared in
8
+ # {https://guides.rubyonrails.org/routing.html#routing-to-rack-applications Rack application}
9
+ # fashion via {ResourcesRouter} to recoganize path in the pattern of `/:mount_path/:resources`.
10
+ # It means when the current request path (e.g. `/admin/categories`)
11
+ # is under the same mount path of {Engine} (e.g. `/admin`),
12
+ # using the original Rails **usl_for** (e.g. `url_for action: :index`)
13
+ # without providing the `:resources` param and script name
14
+ # will lead to an **ActionController::RoutingError** exception.
15
+ #
16
+ # To generate the proper URL from given params and options for this kind of requests,
17
+ # there are three kinds of scenarios that need to be considered
18
+ # (assume that {Engine} is mounted at `/admin`):
19
+ #
20
+ # - if the URL to generate is a regular route defined before mounting the {Engine}
21
+ # that does not override the resources `categories` routes handled by {Engine}, such as:
22
+ #
23
+ # namespace :admin do
24
+ # resources :custom_categories
25
+ # end
26
+ # wallaby_mount at: '/admin'
27
+ #
28
+ # - if the URL to generate is a route that overrides the existing {Engine} route
29
+ # (assume that `categories` is one of the resources handled by {Engine}):
30
+ #
31
+ # namespace :admin do
32
+ # resources :categories
33
+ # end
34
+ # wallaby_mount at: '/admin'
35
+ #
36
+ # - regular resources handled by {ResourcesRouter}, e.g. (`/admin/products`)
5
37
  class EngineUrlFor
6
- # A constant to map actions to their route paths defined in Wallaby routes.
7
- ACTION_TO_PATH_MAP =
8
- Wallaby::ERRORS.each_with_object(ActiveSupport::HashWithIndifferentAccess.new) do |error, map|
9
- map[error] = :"#{error}_path"
10
- end.merge(
11
- home: :root_path,
12
- index: :resources_path,
13
- new: :new_resource_path,
14
- show: :resource_path,
15
- edit: :edit_resource_path
16
- ).freeze
17
-
18
- class << self
19
- # Generate URL that Wallaby engine supports (e.g. home/resourceful/errors)
20
- # @see https://github.com/reinteractive/wallaby/blob/master/config/routes.rb config/routes.rb
21
- # @param engine_name [string]
22
- # @param parameters [ActionController::Parameters, Hash]
23
- # @return [String] path string for wallaby engine
24
- # @return [nil] nil if given engine name cannot be found
25
- def handle(engine_name:, parameters:)
26
- route = Rails.application.routes.named_routes[engine_name]
27
- return unless route
28
-
29
- params = { script_name: route.path.spec.to_s }.merge(parameters).symbolize_keys
30
-
31
- ModuleUtils.try_to(
32
- Engine.routes.url_helpers, action_path_from(params), params
33
- )
34
- end
38
+ include ActiveModel::Model
39
+
40
+ # @!attribute context
41
+ # @return [ActionController::Base, ActionView::Base]
42
+ attr_accessor :context
43
+ # @!attribute options
44
+ # @return [Hash]
45
+ attr_accessor :options
46
+ # @!attribute params
47
+ # @return [Hash, ActionController::Parameters]
48
+ attr_accessor :params
49
+
50
+ # Generate the proper URL depending on the context
51
+ # @param context [ActionController::Base, ActionView::Base]
52
+ # @param params [Hash, ActionController::Parameters]
53
+ # @param options [Hash]
54
+ # @option model_class [Class]
55
+ # @option with_query [true] indicate if all query params should be included
56
+ # @return [nil] nil if params is not a **Hash** or **ActionController::Parameters**
57
+ # @see #execute
58
+ def self.execute(context:, params:, options:)
59
+ return unless params.is_a?(Hash) || params.try(:permitted?)
60
+
61
+ new(context: context, params: params, options: options).execute
62
+ end
63
+
64
+ # @return [String] URL
65
+ # @return [nil] nil if current request is not under any mounted {Engine}
66
+ # @see https://github.com/reinteractive/wallaby/blob/master/config/routes.rb
67
+ def execute
68
+ return if context.current_engine_route.blank?
69
+ return custom_app_route.url if custom_app_route.exist?
35
70
 
36
- protected
71
+ engine_route.url
72
+ end
73
+
74
+ private
75
+
76
+ # @return [Wallaby::Engines::CustomAppRoute]
77
+ def custom_app_route
78
+ @custom_app_route ||=
79
+ Engines::CustomAppRoute.new(
80
+ context: context, params: params, options: options
81
+ )
82
+ end
37
83
 
38
- # Find out the named path from given params
39
- # @return [Symbol] named path
40
- def action_path_from(params)
41
- action = params[:action] || params.fetch(:_recall, {})[:action]
42
- ACTION_TO_PATH_MAP[action]
43
- end
84
+ # @return [Wallaby::Engines::EngineRoute]
85
+ def engine_route
86
+ Engines::EngineRoute.new(
87
+ context: context, params: params, options: options
88
+ )
44
89
  end
45
90
  end
46
91
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # Validate and convert the field metadata to `ActiveSupport::HashWithIndifferentAccess`
5
+ FieldsRegulator = Struct.new(:fields) do
6
+ # @return [ActiveSupport::HashWithIndifferentAccess]
7
+ def execute
8
+ ensure_fields_is_a_hash
9
+ ensure_type_is_present
10
+ normalized_fields
11
+ end
12
+
13
+ protected
14
+
15
+ def ensure_fields_is_a_hash
16
+ return if fields.is_a?(Hash)
17
+
18
+ raise ArgumentError, 'Please provide a Hash metadata'
19
+ end
20
+
21
+ def ensure_type_is_present
22
+ missing_types = normalized_fields.select do |_field, metadata|
23
+ metadata[:type].blank?
24
+ end
25
+ return if missing_types.blank?
26
+
27
+ raise ArgumentError, "Please provide the type for #{missing_types.keys.join(',')}"
28
+ end
29
+
30
+ def normalized_fields
31
+ @normalized_fields ||= fields.with_indifferent_access
32
+ end
33
+ end
34
+ end
@@ -2,14 +2,14 @@
2
2
 
3
3
  module Wallaby
4
4
  class Map
5
- # Go through each {Wallaby::Mode} (e.g. **ActiveRecord**/**Her**)
5
+ # Go through each {Mode} (e.g. **ActiveRecord**/**Her**)
6
6
  # and find out all the model classes respectively.
7
- # Then a hash (Model => {Wallaby::Mode}) is constructed
8
- # to tell {Wallaby} which {Wallaby::Mode} to use for a given model.
7
+ # Then a hash (Model => {Mode}) is constructed
8
+ # to tell {Wallaby} which {Mode} to use for a given model.
9
9
  class ModeMapper
10
10
  extend Classifier
11
11
 
12
- # @param class_names [Wallaby::ClassArray] mode class names
12
+ # @param class_names [ClassArray] mode class names
13
13
  # @return [WallabyClassHash]
14
14
  def self.execute(class_names)
15
15
  ClassHash.new.tap do |hash|
@@ -5,7 +5,7 @@ module Wallaby
5
5
  # Go through the class list and generate a {.map .map} that uses the class's model_class as the key.
6
6
  class ModelClassMapper
7
7
  # @param class_array [Array<Class>]
8
- # @return [Wallaby::ClassHash] model class => descendant class
8
+ # @return [ClassHash] model class => descendant class
9
9
  def self.map(class_array)
10
10
  (class_array || EMPTY_ARRAY).each_with_object(ClassHash.new) do |klass, hash|
11
11
  next if ModuleUtils.anonymous_class?(klass)
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # Fitler the model classes using
5
+ class ModelClassFilter
6
+ class << self
7
+ # @param all [Array<Class>]
8
+ # @param allowlisted [Array<Class>]
9
+ # @param denylisted [Array<Class>]
10
+ def execute(all:, allowlisted:, denylisted:)
11
+ invalid, valid =
12
+ allowlisted.present? ? [allowlisted - all, allowlisted] : [denylisted - all, all - denylisted]
13
+ return valid if invalid.blank?
14
+
15
+ raise InvalidError, <<~INSTRUCTION
16
+ Wallaby can't handle these models: #{invalid.map(&:name).to_sentence}.
17
+ If it's set via controller_class.models= or controller_class.models_to_exclude=,
18
+ please remove them from the list.
19
+
20
+ Or they can be added to Custom model list as below, and custom implementation will be required:
21
+
22
+ Wallaby.config do |config|
23
+ config.custom_models = #{invalid.map(&:name).join ', '}
24
+ end
25
+ INSTRUCTION
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # (see #execute)
5
+ class PaginatorFinder < ServicerFinder
6
+ # Find paginator class by script name and model class from the following places:
7
+ #
8
+ # - {#current_controller_class #current_controller_class}'s
9
+ # {Configurable::ClassMethods#model_paginator #model_paginator}
10
+ # - possible paginator class built from script name and model class,
11
+ # e.g. **/admin** and **Order::Item** will give us the possible paginators:
12
+ # - Admin::Order::ItemPaginator
13
+ # - Order::ItemPaginator
14
+ # - ItemPaginator
15
+ # - {#current_controller_class #current_controller_class}'s default
16
+ # {Configurable::ClassMethods#application_paginator #application_paginator}
17
+ # @return [Class] paginator class
18
+ def execute
19
+ current_controller_class.model_paginator ||
20
+ possible_default_class ||
21
+ current_controller_class.application_paginator
22
+ end
23
+ end
24
+ end
@@ -1,32 +1,73 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wallaby
4
- # To extend prefixes to provide more possibility
4
+ # (see #execute)
5
5
  class PrefixesBuilder
6
6
  include ActiveModel::Model
7
7
 
8
+ delegate :controller_path, to: :controller_class
9
+
10
+ # @!attribute prefixes
11
+ # @return [Array<String>]
8
12
  attr_accessor :prefixes
13
+ # @!attribute resources_name
14
+ # @return [String, nil]
9
15
  attr_accessor :resources_name
16
+ # @!attribute script_name
17
+ # @return [String]
10
18
  attr_accessor :script_name
19
+ # @!attribute controller_class
20
+ # @return [String]
21
+ attr_accessor :controller_class
11
22
 
23
+ # To extend prefixes to be able to look up the directory
24
+ # even if the custom controller doesn't not exist
25
+ # @return [Array<String>] modified prefixes
12
26
  def execute
13
- return if prefixes.include? resources_path
27
+ return prefixes_dup unless resourceful?
28
+ return prefixes_dup if possible_resources_path == controller_path
14
29
 
15
- full_prefix = [script_path, resources_path].compact.join(SLASH)
30
+ prefixes_dup.insert(
31
+ prefixes_dup.index(controller_path) + offset,
32
+ possible_resources_path
33
+ )
34
+ end
16
35
 
17
- return if prefixes.include? full_prefix
36
+ protected
18
37
 
19
- prefixes.insert 0, full_prefix
38
+ # @return [Array<String>]
39
+ def prefixes_dup
40
+ @prefixes_dup ||= prefixes.dup
20
41
  end
21
42
 
22
- private
43
+ # @return [Boolean]
44
+ def resourceful?
45
+ resources_name.present?
46
+ end
47
+
48
+ # @return [Boolean]
49
+ def based_controller?
50
+ controller_class.model_class.blank?
51
+ end
52
+
53
+ # @return [Integer] 0 or 1
54
+ def offset
55
+ based_controller? ? 0 : 1
56
+ end
57
+
58
+ # @return [String]
59
+ def possible_resources_path
60
+ [script_path, resources_path].compact.join(SLASH)
61
+ end
23
62
 
63
+ # @return [String, nil]
24
64
  def resources_path
25
- @resources_path ||= resources_name.try :gsub, COLONS, SLASH
65
+ resources_name.try(:gsub, COLONS, SLASH)
26
66
  end
27
67
 
68
+ # @return [String, nil]
28
69
  def script_path
29
- @script_path ||= script_name.try :[], 1..-1
70
+ script_name.try(:[], 1..-1)
30
71
  end
31
72
  end
32
73
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # (see #execute)
5
+ class ServicerFinder < ControllerFinder
6
+ # Find servicer class by script name and model class from the following places:
7
+ #
8
+ # - {#current_controller_class #current_controller_class}'s
9
+ # {Configurable::ClassMethods#model_servicer #model_servicer}
10
+ # - possible servicer class built from script name and model class,
11
+ # e.g. **/admin** and **Order::Item** will give us the possible servicers:
12
+ # - Admin::Order::ItemServicer
13
+ # - Order::ItemServicer
14
+ # - ItemServicer
15
+ # - {#current_controller_class #current_controller_class}'s default
16
+ # {Configurable::ClassMethods#application_servicer #application_servicer}
17
+ # @return [Class] servicer class
18
+ def execute
19
+ current_controller_class.model_servicer ||
20
+ possible_default_class ||
21
+ current_controller_class.application_servicer
22
+ end
23
+
24
+ protected
25
+
26
+ # (see ClassFinder#denamespace?)
27
+ def denamespace?
28
+ true
29
+ end
30
+ end
31
+ end
@@ -14,6 +14,15 @@ module Wallaby
14
14
  (sort_string || EMPTY_STRING).scan(SORT_REGEX) { |_, key, order| hash[key] = order }
15
15
  end
16
16
  end
17
+
18
+ def self.to_str(hash)
19
+ hash.each_with_object(EMPTY_STRING.dup) do |(name, sort), str|
20
+ next unless /\A(asc|desc)( nulls (first|last))?\Z/i.match?(sort)
21
+
22
+ str << (str == EMPTY_STRING ? str : COMMA)
23
+ str << name.to_s << SPACE << sort << (block_given? ? yield(name) : EMPTY_STRING)
24
+ end
25
+ end
17
26
  end
18
27
  end
19
28
  end
@@ -9,7 +9,7 @@ module Wallaby
9
9
  delegate :model_class, to: :@model_decorator
10
10
  delegate :index_link, :url_for, to: :@helper
11
11
 
12
- # @param model_decorator [Wallaby::ModelDecorator]
12
+ # @param model_decorator [ModelDecorator]
13
13
  # @param params [ActionController::Parameters]
14
14
  # @param helper [ActionView::Helpers]
15
15
  def initialize(model_decorator, params, helper, strategy)
@@ -28,15 +28,11 @@ module Wallaby
28
28
 
29
29
  # Build sort link for given field name:
30
30
  #
31
- # ```
32
- # <a title="Product" href="/admin/products?sort=published_at+asc">Name</a>
33
- # ```
31
+ # <a title="Product" href="/admin/products?sort=published_at+asc">Name</a>
34
32
  #
35
33
  # If the field is not sortable, it returns a text, e.g.:
36
34
  #
37
- # ```
38
- # Name
39
- # ```
35
+ # Name
40
36
  # @param field_name [String]
41
37
  # @return [String] link or text
42
38
  def build(field_name)
@@ -46,13 +42,14 @@ module Wallaby
46
42
 
47
43
  sort_field_name = metadata[:sort_field_name] || field_name
48
44
  url_params = next_builder.next_params sort_field_name
49
- url = url_for url_params.merge(with_query: true)
50
- index_link(model_class, options: { url: url }) { label }
45
+ index_link(
46
+ model_class, url_params: url_params.merge(with_query: true)
47
+ ) { label }
51
48
  end
52
49
 
53
50
  private
54
51
 
55
- # @return [Wallaby::Sorting::NextBuilder]
52
+ # @return [Sorting::NextBuilder]
56
53
  def next_builder
57
54
  @next_builder ||= begin
58
55
  klass = SORT_STRATEGIES[@strategy] || NextBuilder
@@ -35,18 +35,7 @@ module Wallaby
35
35
  hash = @hash.except field_name
36
36
  current_sort = @hash[field_name]
37
37
  hash[field_name] = next_value_for current_sort
38
- rebuild_str_from hash
39
- end
40
-
41
- # @param hash [Hash] sort order hash
42
- # @return [String] a sort order string, e.g. `'name asc'`
43
- def rebuild_str_from(hash)
44
- hash.each_with_object(EMPTY_STRING.dup) do |(name, sort), str|
45
- next unless sort
46
-
47
- str << (str == EMPTY_STRING ? str : COMMA)
48
- str << name.to_s << SPACE << sort
49
- end
38
+ HashBuilder.to_str(hash)
50
39
  end
51
40
 
52
41
  # @param current [String, nil] current sort order
@@ -13,7 +13,7 @@ module Wallaby
13
13
  hash = {}
14
14
  current_sort = @hash[field_name]
15
15
  hash[field_name] = next_value_for current_sort
16
- rebuild_str_from hash
16
+ HashBuilder.to_str(hash)
17
17
  end
18
18
  end
19
19
  end
@@ -2,8 +2,33 @@
2
2
 
3
3
  module ActionDispatch
4
4
  module Routing
5
- # Re-open `ActionDispatch::Routing::Mapper` to add route helpers for Wallaby.
5
+ # Re-open {https://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper.html
6
+ # ActionDispatch::Routing::Mapper}
7
+ # to add route helpers for {Wallaby}.
6
8
  class Mapper
9
+ # Mount {Wallaby::Engine} at given path.
10
+ # And prepend custom routes to Rails app if block is given.
11
+ # @example Mount {Wallaby::Engine} at a path
12
+ # wallaby_mount at: '/admin'
13
+ # @example Mount {Wallaby::Engine} and prepend custom routes
14
+ # wallaby_mount at: '/super_admin' do
15
+ # resource :accounts
16
+ # end
17
+ # # the above code is the same as:
18
+ # namespace :super_admin do
19
+ # resource :accounts
20
+ # end
21
+ # mount Wallaby::Engine, at: '/super_admin'
22
+ # @param options [Hash]
23
+ # @option options [String] :at the path which {Wallaby::Engine} is mounted at
24
+ # @option options [Symbol, String] :as {Wallaby::Engine} alias name for URL helpers
25
+ # @option options [Array] :via HTTP methods that {Wallaby::Engine} processes
26
+ def wallaby_mount(options, &block)
27
+ # define routes under namespace (e.g. `:admin`) before mounting the {Wallaby:Engine} (e.g. at `/admin`)
28
+ namespace(options[:at][1..] || '', options.except(:at), &block) if block
29
+ mount Wallaby::Engine, options.slice(:at, :as, :via)
30
+ end
31
+
7
32
  # Generate **resourceful** routes that works for Wallaby.
8
33
  # @example To generate resourceful routes that works for Wallaby:
9
34
  # wresources :postcodes
@@ -53,7 +78,7 @@ module ActionDispatch
53
78
  # @param options [Hash]
54
79
  def wallaby_resources_options_for(resources_name, options)
55
80
  { path: ':resources' }.merge!(options).tap do |new_options|
56
- %i(defaults constraints).each do |key|
81
+ %i[defaults constraints].each do |key|
57
82
  new_options[key] = { resources: resources_name }.merge!(new_options[key] || {})
58
83
  end
59
84
  end
@@ -63,9 +88,9 @@ module ActionDispatch
63
88
  # @param resource_name [String, Symbol]
64
89
  # @param options [Hash]
65
90
  def wallaby_resource_options_for(resource_name, options)
66
- plural_resources = Wallaby::ModelUtils.to_resources_name resource_name
91
+ plural_resources = Wallaby::Inflector.to_resources_name resource_name
67
92
  { path: ':resource' }.merge!(options).tap do |new_options|
68
- %i(defaults constraints).each do |key|
93
+ %i[defaults constraints].each do |key|
69
94
  new_options[key] = { resource: resource_name, resources: plural_resources }.merge!(new_options[key] || {})
70
95
  end
71
96
  end
@@ -7,8 +7,8 @@ module Wallaby
7
7
  # Find the first field that meets the first condition.
8
8
  # @example to find the possible text field
9
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`
10
+ # # => if any field name that includes `name` or `title`, return this field
11
+ # # => otherwise, find the first field whose type is `string`
12
12
  # @param conditions [Array<Hash>]
13
13
  # @param fields [Hash] field metadata
14
14
  # @return [String, Symbol] field name
@@ -17,25 +17,26 @@ module Wallaby
17
17
 
18
18
  conditions.each do |condition|
19
19
  fields.each do |field_name, metadata|
20
- return field_name if meet? field_name, metadata.with_indifferent_access, condition
20
+ return field_name if meet? condition, field_name, metadata
21
21
  end
22
22
  end
23
+
23
24
  nil
24
25
  end
25
26
 
26
27
  protected
27
28
 
29
+ # @param condition [Hash]
28
30
  # @param field_name [String]
29
31
  # @param metadata [Hash]
30
- # @param condition [Hash]
31
32
  # @return [true] if field's metadata meets the condition
32
33
  # @return [true] otherwise
33
- def meet?(field_name, metadata, condition)
34
+ def meet?(condition, field_name, metadata)
34
35
  condition.all? do |key, requirement|
35
- operator = requirement.is_a?(::Regexp) ? '=~' : '=='
36
- value = metadata[key]
36
+ value = metadata.with_indifferent_access[key]
37
37
  value ||= field_name.to_s if key.to_sym == :name
38
- ModuleUtils.try_to value, operator, requirement
38
+ value = value.to_s if value.is_a? Symbol
39
+ requirement === value
39
40
  end
40
41
  end
41
42
  end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # Convert strings
5
+ module Inflector
6
+ extend self
7
+
8
+ # @param name [String]
9
+ # @return [String] class name
10
+ # @return [nil] if name is blank
11
+ def to_class_name(name)
12
+ return EMPTY_STRING if name.blank?
13
+
14
+ name = name.to_s unless name.is_a?(String)
15
+ name.gsub(COLONS, SLASH).classify
16
+ end
17
+
18
+ # @param script_name [String]
19
+ # @param resources_name [Class,String]
20
+ # @param suffix [String,nil]
21
+ # @return [String] generate the prefix for all classes
22
+ def to_script(script_name, resources_name, suffix = nil)
23
+ normalized_suffix = suffix.try(:underscore).try(:gsub, /\A_?/, UNDERSCORE)
24
+ "#{script_name}/#{resources_name}#{normalized_suffix}".gsub(%r{\A/+}, EMPTY_STRING)
25
+ end
26
+
27
+ # @param script_name [String]
28
+ # @param resources_name [Class,String]
29
+ # @return [String] controller name
30
+ def to_controller_name(script_name, resources_name)
31
+ to_class_name(to_script(script_name, to_resources_name(resources_name), CONTROLLER))
32
+ end
33
+
34
+ # @param script_name [String]
35
+ # @param resources_name [Class,String]
36
+ # @return [String] decorator name
37
+ def to_decorator_name(script_name, resources_name)
38
+ to_class_name(to_script(script_name, to_resource_name(resources_name), DECORATOR))
39
+ end
40
+
41
+ # @param script_name [String]
42
+ # @param resources_name [Class,String]
43
+ # @return [String] authorizer name
44
+ def to_authorizer_name(script_name, resources_name)
45
+ to_class_name(to_script(script_name, to_resource_name(resources_name), AUTHORIZER))
46
+ end
47
+
48
+ # @param script_name [String]
49
+ # @param resources_name [Class,String]
50
+ # @return [String] servicer name
51
+ def to_servicer_name(script_name, resources_name)
52
+ to_class_name(to_script(script_name, to_resource_name(resources_name), SERVICER))
53
+ end
54
+
55
+ # @param script_name [String]
56
+ # @param resources_name [Class,String]
57
+ # @return [String] paginator name
58
+ def to_paginator_name(script_name, resources_name)
59
+ to_class_name(to_script(script_name, to_resource_name(resources_name), PAGINATOR))
60
+ end
61
+
62
+ # @param name [Class, String]
63
+ # @return [String] resources name
64
+ def to_resources_name(name)
65
+ return EMPTY_STRING if name.blank?
66
+
67
+ name = name.to_s unless name.is_a?(String)
68
+ name.tableize.gsub(SLASH, COLONS)
69
+ end
70
+
71
+ # @param name [Class, String]
72
+ # @return [String] resource name
73
+ def to_resource_name(name)
74
+ to_resources_name(name).singularize
75
+ end
76
+
77
+ # @param name [Class, String]
78
+ # @return [String] resource name
79
+ def to_model_name(name)
80
+ to_class_name(to_resource_name(name))
81
+ end
82
+
83
+ # Produce model label (e.g. `Namespace / Product`) for model class (e.g. `Namespace::Product`)
84
+ # @param model_class [Class, String] model class
85
+ # @return [String] model label
86
+ def to_model_label(model_class)
87
+ # TODO: change to use i18n translation
88
+ return EMPTY_STRING if model_class.blank?
89
+
90
+ model_class_name = to_model_name model_class
91
+ model_class_name.titleize.gsub(SLASH, SPACE + SLASH + SPACE)
92
+ end
93
+ end
94
+ end
@@ -11,11 +11,11 @@ module Wallaby
11
11
  # @return [String] translation
12
12
  def t(key, options = {})
13
13
  translator = options.delete(:translator) || I18n.method(:t)
14
- return translator.call(key, **options) unless key.is_a?(String) || key.is_a?(Symbol)
14
+ return translator.call(key, options) unless key.is_a?(String) || key.is_a?(Symbol)
15
15
 
16
16
  new_key, new_defaults = normalize key, options.delete(:default)
17
17
 
18
- translator.call(new_key, **{ default: new_defaults }.merge(options))
18
+ translator.call(new_key, default: new_defaults, **options)
19
19
  end
20
20
 
21
21
  private