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,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # Service object to find the engine name by given request environment variables.
5
+ class EngineNameFinder
6
+ class << self
7
+ # Use script name to find out the corresponding named route and its engine name.
8
+ #
9
+ # When it can't find the engine name, it will return empty string. Reason is to prevent it from being run again.
10
+ # @param env [Hash, String] request env or script name
11
+ # @return [String] engine name or empty string if not found
12
+ def find(env)
13
+ script_name = env[SCRIPT_NAME] || env[PATH_INFO] if env.is_a? Hash
14
+ script_name = env if env.is_a? String
15
+ return EMPTY_STRING if script_name.blank?
16
+
17
+ named_route = Rails.application.routes.routes.find { |route| route.path.match(script_name) }
18
+ named_route.try(:name) || EMPTY_STRING
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # {Wallaby::ApplicationHelper#url_for} helper for Wallaby engine
5
+ 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
35
+
36
+ protected
37
+
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
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # Link renderer
5
+ class LinkOptionsNormalizer
6
+ # Normalize options for `link_to` to use
7
+ # @param html_options [Hash] HTML options
8
+ # @param block [Proc] a block
9
+ # @param defaults [Hash]
10
+ # @return [Array<Hash, Proc>] html_options and the block
11
+ def self.normalize(html_options, block, defaults)
12
+ block ||= defaults[:block]
13
+ html_options[:title] ||= defaults[:block].call
14
+ # allow empty class to be set
15
+ html_options[:class] = defaults[:class] if !html_options.key?(:class) && defaults[:class]
16
+ [html_options, block]
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ class Map
5
+ # To generate a hash map (`model` => `mode`).
6
+ # This will be used to tell if a model can be handled by Wallaby
7
+ class ModeMapper
8
+ # @param mode_classes [Array<Class>] model classes
9
+ def initialize(mode_classes)
10
+ @mode_classes = mode_classes
11
+ end
12
+
13
+ # This will walk through each mode (e.g. **ActiveRecord**/**Her**) then pull out all the models,
14
+ # and then form a hash of (`model` => `mode`).
15
+ # @return [Hash] { model_class => mode }
16
+ def map
17
+ return {} if @mode_classes.blank?
18
+
19
+ @mode_classes.each_with_object({}) do |mode_class, map|
20
+ mode_class.model_finder.new.all.each do |model_class|
21
+ map[model_class] = mode_class
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ class Map
5
+ # To collect model classes that are configured to be handled by Wallaby
6
+ class ModelClassCollector
7
+ # @param configuration [Configuration]
8
+ # @param models [Array<Class>]
9
+ def initialize(configuration, models = nil)
10
+ @configuration = configuration
11
+ @models = models || []
12
+ end
13
+
14
+ # @return [Array<Class>] model class
15
+ def collect
16
+ return @models - excluded_models if configured_models.blank?
17
+
18
+ invalid_models_check
19
+ configured_models
20
+ end
21
+
22
+ private
23
+
24
+ # Check if the models are valid, raise if invalid
25
+ def invalid_models_check
26
+ invalid_models = configured_models - @models
27
+ return if invalid_models.blank?
28
+
29
+ message = I18n.t 'errors.invalid.models', models: invalid_models.to_sentence
30
+ raise InvalidError, message
31
+ end
32
+
33
+ # @return [Wallaby::Configuration::Models]
34
+ def models
35
+ @configuration.models
36
+ end
37
+
38
+ # @return [Array<Class>] a list of models to exclude
39
+ def excluded_models
40
+ models.excludes
41
+ end
42
+
43
+ # @return [Array<Class>] a list of models to set
44
+ def configured_models
45
+ models.presence
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ class Map
5
+ # Generate a map.
6
+ class ModelClassMapper
7
+ # Iterate all classes and generate a hash using their model classes as the key
8
+ # @see #map
9
+ # @param class_array [Array<Class>]
10
+ # @return [Hash] model class => descendant class
11
+ def self.map(class_array, &block)
12
+ new.send :map, class_array, &block
13
+ end
14
+
15
+ protected
16
+
17
+ # @return [Hash] model class => descendant class
18
+ def map(class_array)
19
+ (class_array || EMPTY_ARRAY).each_with_object({}) do |klass, map|
20
+ next if anonymous?(klass) || base_class?(klass) || !klass.model_class
21
+
22
+ map[klass.model_class] = block_given? ? yield(klass) : klass
23
+ end
24
+ end
25
+
26
+ # @see Wallaby::ModuleUtils.anonymous_class?
27
+ def anonymous?(klass)
28
+ ModuleUtils.anonymous_class? klass
29
+ end
30
+
31
+ # @param klass [Class]
32
+ # @return [Boolean] whether the class is base or not
33
+ def base_class?(klass)
34
+ ModuleUtils.try_to klass, :base_class?
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # To extend prefixes to provide more possibility
5
+ class PrefixesBuilder
6
+ # @param origin_prefixes [Array<string>] a list of all the origin prefixes
7
+ # @param action_name [String] action name
8
+ # @param resources_name [String] resources name
9
+ # @param script_name [String] script name
10
+ # @param theme_name [String] theme name
11
+ def self.build(origin_prefixes:, action_name:, resources_name:, script_name:, theme_name:)
12
+ new(
13
+ origin_prefixes: origin_prefixes,
14
+ action_name: action_name,
15
+ resources_name: resources_name,
16
+ script_name: script_name,
17
+ theme_name: theme_name
18
+ ).send :build
19
+ end
20
+
21
+ private
22
+
23
+ # Constructor
24
+ def initialize(origin_prefixes:, action_name:, resources_name:, script_name:, theme_name:)
25
+ @origin_prefixes = origin_prefixes
26
+ @action_name = action_name
27
+ @resources_name = resources_name
28
+ @script_name = script_name
29
+ @theme_name = theme_name
30
+ end
31
+
32
+ # @return [Array<String>] prefixes
33
+ def build
34
+ prefixes = minimal_prefixes
35
+ prefixes.unshift mounted_prefix unless prefixes.include? resources_path
36
+ prefixes.uniq.compact.each_with_object([]) do |prefix, result|
37
+ result << "#{prefix}/#{suffix}" << prefix
38
+ end
39
+ end
40
+
41
+ # @return [Array<String>] prefixes starting from wallaby controller path
42
+ def minimal_prefixes
43
+ index = @origin_prefixes.index ResourcesController.controller_path
44
+ @origin_prefixes.slice(0..index).tap do |prefixes|
45
+ insert_index = prefixes.length > 1 ? -2 : 0
46
+ prefixes.insert insert_index, @theme_name if @theme_name.present?
47
+ end
48
+ end
49
+
50
+ # @return [String] a prefix of the mouted path
51
+ def mounted_prefix
52
+ prefix = (@script_name || '').sub(%r{^/}, '')
53
+ prefix << SLASH if prefix.present?
54
+ prefix << resources_path if resources_path
55
+ end
56
+
57
+ # @return [String]
58
+ def suffix
59
+ @suffix ||= CellUtils.to_action_prefix @action_name
60
+ end
61
+
62
+ def resources_path
63
+ @resources_path ||= @resources_name.try :gsub, COLONS, SLASH
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ module Sorting
5
+ # Turn a string e.g.`'name asc,id desc'` into sort hash e.g.`{name: 'asc', id: 'desc'}`
6
+ class HashBuilder
7
+ SORT_REGEX = /(([^\s,]+)\s+(asc|desc))\s*,?\s*/i.freeze
8
+
9
+ # Turn a string e.g.`'name asc,id desc'` into sort hash e.g.`{name: 'asc', id: 'desc'}`
10
+ # @param sort_string [String]
11
+ # @return [Hash] { field_name => 'asc|desc' }
12
+ def self.build(sort_string)
13
+ ::ActiveSupport::HashWithIndifferentAccess.new.tap do |hash|
14
+ (sort_string || EMPTY_STRING).scan(SORT_REGEX) { |_, key, order| hash[key] = order }
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ module Sorting
5
+ # Build link for sorting
6
+ class LinkBuilder
7
+ SORT_STRATEGIES = { single: SingleBuilder }.with_indifferent_access.freeze
8
+
9
+ delegate :model_class, to: :@model_decorator
10
+ delegate :index_link, :url_for, to: :@helper
11
+
12
+ # @param model_decorator [Wallaby::ModelDecorator]
13
+ # @param params [ActionController::Parameters]
14
+ # @param helper [ActionView::Helpers]
15
+ def initialize(model_decorator, params, helper, strategy)
16
+ @model_decorator = model_decorator
17
+ @params = params
18
+ @helper = helper
19
+ @strategy = strategy
20
+ end
21
+
22
+ # To return the sort hash converted from string value, e.g. `{ 'title' => 'asc', 'updated_at' => 'desc' }`
23
+ # converted from `'title asc, updated_at desc'`
24
+ # @return [Hash] current sort hash
25
+ def current_sort
26
+ @current_sort ||= HashBuilder.build @params[:sort]
27
+ end
28
+
29
+ # Build sort link for given field name:
30
+ #
31
+ # ```
32
+ # <a title="Product" href="/admin/products?sort=published_at+asc">Name</a>
33
+ # ```
34
+ #
35
+ # If the field is not sortable, it returns a text, e.g.:
36
+ #
37
+ # ```
38
+ # Name
39
+ # ```
40
+ # @param field_name [String]
41
+ # @return [String] link or text
42
+ def build(field_name)
43
+ metadata = @model_decorator.index_metadata_of field_name
44
+ label = @model_decorator.index_label_of field_name
45
+ return label unless sortable? field_name, metadata
46
+
47
+ sort_field_name = metadata[:sort_field_name] || field_name
48
+ 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 }
51
+ end
52
+
53
+ private
54
+
55
+ # @return [Wallaby::Sorting::NextBuilder]
56
+ def next_builder
57
+ @next_builder ||= begin
58
+ klass = SORT_STRATEGIES[@strategy] || NextBuilder
59
+ klass.new @params, current_sort
60
+ end
61
+ end
62
+
63
+ # @return [Boolean] true if sortable or `sort_field_name` is provided in the metadata
64
+ def sortable?(field_name, metadata)
65
+ !metadata[:sort_disabled] && (@model_decorator.fields[field_name] || metadata[:sort_field_name])
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ module Sorting
5
+ # Generate sort param for given field's next sort order
6
+ # (e.g. from empty to `asc`, from `asc` to `desc`, from `desc` to empty)
7
+ class NextBuilder
8
+ ASC = 'asc'
9
+ DESC = 'desc'
10
+
11
+ # @param params [ActionController::Parameters]
12
+ # @param hash [Hash, nil] a hash containing sorting info, e.g. `{ name: 'asc' }`
13
+ def initialize(params, hash = nil)
14
+ @params = params
15
+ @hash = hash.try(:with_indifferent_access) || HashBuilder.build(params[:sort])
16
+ end
17
+
18
+ # Update the `sort` parameter.
19
+ # @example for param `{sort: 'name asc'}`, it updates the parameters to:
20
+ # {sort: 'name desc'}
21
+ # @param field_name [String] field name
22
+ # @return [ActionController::Parameters]
23
+ # updated parameters with new sort order for given field
24
+ def next_params(field_name)
25
+ params = @params.dup
26
+ params[:sort] = complete_sorting_str_with field_name
27
+ params
28
+ end
29
+
30
+ protected
31
+
32
+ # @param field_name [String] field name
33
+ # @return [String] a sort order string, e.g. `'name asc'`
34
+ def complete_sorting_str_with(field_name)
35
+ hash = @hash.except field_name
36
+ current_sort = @hash[field_name]
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
50
+ end
51
+
52
+ # @param current [String, nil] current sort order
53
+ # @return [String, nil] next sort order
54
+ def next_value_for(current)
55
+ case current
56
+ when ASC then DESC
57
+ when DESC then nil
58
+ else ASC
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ module Sorting
5
+ # Generate sort param for only one field's next sort order
6
+ # (e.g. from empty to `asc`, from `asc` to `desc`, from `desc` to empty)
7
+ class SingleBuilder < NextBuilder
8
+ protected
9
+
10
+ # @param field_name [String] field name
11
+ # @return [String] a sort order string, e.g. `'name asc'`
12
+ def complete_sorting_str_with(field_name)
13
+ hash = {}
14
+ current_sort = @hash[field_name]
15
+ hash[field_name] = next_value_for current_sort
16
+ rebuild_str_from hash
17
+ end
18
+ end
19
+ end
20
+ end