wallaby-core 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # Resources helper
5
+ module ResourcesHelper
6
+ include BaseHelper
7
+ include FormHelper
8
+ include IndexHelper
9
+
10
+ include Authorizable
11
+ include Decoratable
12
+ include Paginatable
13
+ include Resourcable
14
+ include Servicable
15
+ include Themeable
16
+
17
+ # @deprecated Use {#type_render} instead. It will be removed from 5.3.*
18
+ def type_partial_render(options = {}, locals = {}, &block)
19
+ Utils.deprecate 'deprecation.type_partial_render', caller: caller
20
+ type_render options, locals, &block
21
+ end
22
+
23
+ # Render type cell/partial
24
+ # @param partial_name [String]
25
+ # @param locals [Hash]
26
+ def type_render(partial_name = '', locals = {}, &block)
27
+ TypeRenderer.render self, partial_name, locals, &block
28
+ end
29
+
30
+ # Title for show page of given resource
31
+ # @param decorated [Wallaby::ResourceDecorator]
32
+ # @return [String]
33
+ def show_title(decorated)
34
+ raise ::ArgumentError unless decorated.is_a? ResourceDecorator
35
+
36
+ [
37
+ to_model_label(decorated.model_class), decorated.to_label
38
+ ].compact.join ': '
39
+ end
40
+
41
+ # To find the first field that meets given conditions.
42
+ # @example To find summary field whose name contains _summary_ and type is **string**:
43
+ # first_field_by({ name: /summary/, type: 'string' })
44
+ # @param conditions [Array<Hash>]
45
+ # @return [String, Symbol] field name when found
46
+ # @return [nil] when not found
47
+ def first_field_by(*conditions)
48
+ fields = block_given? ? yield : current_fields
49
+ FieldUtils.first_field_by(*conditions, fields)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # Secure helper
5
+ module SecureHelper
6
+ # Image portrait for given user.
7
+ #
8
+ # - if email is present, a gravatar image tag will be returned
9
+ # - otherwise, an user icon will be returned
10
+ # @param user [Object]
11
+ # @return [String] IMG or I element
12
+ def user_portrait(user = current_user)
13
+ email_method = security.email_method || :email
14
+ email = try_to user, email_method
15
+ if email.present?
16
+ https = "http#{request.ssl? ? 's' : EMPTY_STRING}"
17
+ email_md5 = ::Digest::MD5.hexdigest email.downcase
18
+ image_source = "#{https}://www.gravatar.com/avatar/#{email_md5}"
19
+ image_tag image_source, class: 'user'
20
+ else
21
+ fa_icon 'user'
22
+ end
23
+ end
24
+
25
+ # Logout path for given user
26
+ # @see Wallaby::Configuration::Security#logout_path
27
+ # @param user [Object]
28
+ # @param app [Object]
29
+ # @return [String] URL to log out
30
+ def logout_path(user = current_user, app = main_app)
31
+ path = security.logout_path
32
+ path ||=
33
+ if defined? ::Devise
34
+ scope = ::Devise::Mapping.find_scope! user
35
+ "destroy_#{scope}_session_path"
36
+ end
37
+ try_to app, path
38
+ end
39
+
40
+ # Logout method for given user
41
+ # @see Wallaby::Configuration::Security#logout_method
42
+ # @param user [Object]
43
+ # @return [String, Symbol] http method to log out
44
+ def logout_method(user = current_user)
45
+ http_method = security.logout_method
46
+ http_method ||
47
+ if defined? ::Devise
48
+ scope = ::Devise::Mapping.find_scope! user
49
+ mapping = ::Devise.mappings[scope]
50
+ mapping.sign_out_via
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # Helper methods to build custom elements
5
+ module StylingHelper
6
+ # Shortcut to build up the HTML options as keyword arguments
7
+ # @param string_or_array [String, Array<String>]
8
+ # @param options [Hash]
9
+ # @return [Hash]
10
+ def html_classes(string_or_array, options = {})
11
+ { html_options: options.merge(class: string_or_array) }
12
+ end
13
+
14
+ # Shortcut to generate FontAwesome icon using tag <i>.
15
+ # @overload fa_icon(*names, html_options)
16
+ # @param names [Array<String>] names of the icon
17
+ # @param html_options [Hash] HTML options for tag <i>
18
+ # @return [String] HTML I element
19
+ def fa_icon(*args, &block)
20
+ html_options = args.extract_options!
21
+ html_options[:class] = Array html_options[:class]
22
+ html_options[:class] << 'fa'
23
+ args.each { |suffix| html_options[:class] << "fa-#{fa_map suffix}" }
24
+
25
+ content_tag :i, nil, html_options, &block
26
+ end
27
+
28
+ # Build up tooltip
29
+ # @param title [String]
30
+ # @param icon_suffix [String]
31
+ # @param html_options [Hash]
32
+ # @return [String] tooltip HTML
33
+ def itooltip(title, icon_suffix = 'info-circle', html_options = {})
34
+ html_options[:title] = title
35
+ (html_options[:data] ||= {}).merge! toggle: 'tooltip', placement: 'top'
36
+
37
+ fa_icon icon_suffix, html_options
38
+ end
39
+
40
+ # Build up modal
41
+ # @param title [String]
42
+ # @param body [String]
43
+ # @param html_options [Hash]
44
+ # @return [String] modal HTML
45
+ def imodal(title, body, html_options = {})
46
+ label ||= html_options.delete(:label) \
47
+ || html_options.delete(:icon) || fa_icon('clone')
48
+ content_tag :span, class: 'modaler' do
49
+ concat link_to(label, '#', data: { target: '#imodal', toggle: 'modal' })
50
+ concat content_tag(:span, title, class: 'modaler__title')
51
+ concat content_tag(:span, body, class: 'modaler__body')
52
+ end
53
+ end
54
+
55
+ # @return [String] grey null
56
+ def null
57
+ muted t 'labels.empty'
58
+ end
59
+
60
+ # @return [String] grey N/A
61
+ def na
62
+ muted t 'labels.na'
63
+ end
64
+
65
+ # Grey text
66
+ # @param text_content [String]
67
+ # @return [String] HTML I element
68
+ def muted(text_content)
69
+ content_tag :i, "<#{text_content}>", class: 'text-muted'
70
+ end
71
+
72
+ # @param name [String]
73
+ # @return [String] FontAwesome icon name
74
+ def fa_map(name, major = nil)
75
+ @map ||= begin
76
+ major ||= Gem.loaded_specs['font-awesome-sass'].try(:version).try(:segments).try(:first)
77
+ t("fa.v#{major}").with_indifferent_access
78
+ end
79
+ @map[name] || name
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # This is the interface that all ORM modes should inherit from and implement.
5
+ class Mode
6
+ class << self
7
+ # @see Wallaby::ModelDecorator
8
+ # @return [Class] model decorator for the mode
9
+ def model_decorator
10
+ check_and_constantize __callee__
11
+ end
12
+
13
+ # @see Wallaby::ModelFinder
14
+ # @return [Class] model finder for the mode
15
+ def model_finder
16
+ check_and_constantize __callee__
17
+ end
18
+
19
+ # @see Wallaby::ModelServiceProvider
20
+ # @return [Class] service provider for the mode
21
+ def model_service_provider
22
+ check_and_constantize __callee__
23
+ end
24
+
25
+ # @see Wallaby::ModelPaginationProvider
26
+ # @return [Class] pagination provider for the mode
27
+ # @since 5.2.0
28
+ def model_pagination_provider
29
+ check_and_constantize __callee__
30
+ end
31
+
32
+ # @see Wallaby::ModelPaginationProvider
33
+ # @return [Class] pagination provider for the mode
34
+ # @since 5.2.0
35
+ def default_authorization_provider
36
+ check_and_constantize __callee__
37
+ end
38
+
39
+ # Return a list of authorization providers for authorizer to detect which one to use.
40
+ # @see Wallaby::ModelAuthorizationProvider
41
+ # @return [ActiveSupport::HashWithIndifferentAccess<String, Class>] authorization provider hash
42
+ # @since 5.2.0
43
+ def model_authorization_providers(classes = ModelAuthorizationProvider.descendants)
44
+ # NOTE: make sure default provider always comes last
45
+ @model_authorization_providers ||=
46
+ classes
47
+ .select { |klass| klass.name.include? name }
48
+ .sort_by { |klass| klass.provider_name == DEFAULT_PROVIDER ? 1 : 0 }
49
+ .each_with_object(::ActiveSupport::HashWithIndifferentAccess.new) do |klass, hash|
50
+ hash[klass.provider_name] = klass
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ # @return [Class] constantized class
57
+ def check_and_constantize(method_id)
58
+ method_class = method_id.to_s.classify
59
+ class_name = "#{name}::#{method_class}"
60
+ parent_class = "Wallaby::#{method_class}".constantize
61
+ class_name.constantize.tap do |klass|
62
+ next if klass < parent_class
63
+
64
+ raise InvalidError, I18n.t('wallaby.mode.inherit_required', klass: klass, parent: parent_class)
65
+ end
66
+ rescue NameError => e
67
+ Rails.logger.error e
68
+ raise NotImplemented, class_name
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # Model Authorizer interface.
5
+ # @since 5.2.0
6
+ class ModelAuthorizationProvider
7
+ class << self
8
+ # @!attribute [w] provider_name
9
+ attr_writer :provider_name
10
+
11
+ # @!attribute [r] provider_name
12
+ # This is the provider name that can be set in Wallaby::ModelAuthorizer subclasses.
13
+ # @see Wallaby::ModelAuthorizer.provider_name
14
+ # @return [String/Symbol] provider name
15
+ def provider_name
16
+ @provider_name || name.demodulize.gsub(/(Authorization)?Provider/, EMPTY_STRING).underscore
17
+ end
18
+
19
+ # @note Template method to check and see if current provider is in used.
20
+ # @param _context [ActionController::Base]
21
+ # @raise [Wallaby::NotImplemented]
22
+ def available?(_context)
23
+ raise NotImplemented
24
+ end
25
+
26
+ # @note Template method to pull out the args required for contruction from context.
27
+ # @param _context [ActionController::Base]
28
+ # @raise [Wallaby::NotImplemented]
29
+ def args_from(_context)
30
+ raise NotImplemented
31
+ end
32
+ end
33
+
34
+ # @!attribute [r] context
35
+ # @return [ActionController::Base]
36
+ attr_reader :context
37
+
38
+ # @!attribute [r] user
39
+ # @return [Object]
40
+ attr_reader :user
41
+
42
+ # Empty initialize that accepts all sorts of args
43
+ def initialize(*); end
44
+
45
+ # @note It can be overridden in subclasses for customization purpose.
46
+ # This is the template method to check user's permission for given action on given subject.
47
+ # @param _action [Symbol, String]
48
+ # @param _subject [Object, Class]
49
+ # @raise [Wallaby::NotImplemented]
50
+ def authorize(_action, _subject)
51
+ raise NotImplemented
52
+ end
53
+
54
+ # @note It can be overridden in subclasses for customization purpose.
55
+ # This is the template method to check if user has permission for given action on given subject.
56
+ # @param _action [Symbol, String]
57
+ # @param _subject [Object, Class]
58
+ # @raise [Wallaby::NotImplemented]
59
+ def authorized?(_action, _subject)
60
+ raise NotImplemented
61
+ end
62
+
63
+ # @note It can be overridden in subclasses for customization purpose.
64
+ # This is the template method to check if user has no permission for given action on given subject.
65
+ # @param action [Symbol, String]
66
+ # @param subject [Object, Class]
67
+ # @raise [Wallaby::NotImplemented]
68
+ def unauthorized?(action, subject)
69
+ !authorized?(action, subject)
70
+ end
71
+
72
+ # @note It can be overridden in subclasses for customization purpose.
73
+ # This is the template method to restrict user's access to certain scope.
74
+ # @param _action [Symbol, String]
75
+ # @param _scope [Object]
76
+ # @raise [Wallaby::NotImplemented]
77
+ def accessible_for(_action, _scope)
78
+ raise NotImplemented
79
+ end
80
+
81
+ # @note It can be overridden in subclasses for customization purpose.
82
+ # This is the template method to restrict user's modification to certain fields of given subject.
83
+ # @param _action [Symbol, String]
84
+ # @param _subject [Object]
85
+ # @raise [Wallaby::NotImplemented]
86
+ def attributes_for(_action, _subject)
87
+ raise NotImplemented
88
+ end
89
+
90
+ # @note It can be overridden in subclasses for customization purpose.
91
+ # This is the template method to restrict user's mass assignment to certain fields of given subject.
92
+ # @param _action [Symbol, String]
93
+ # @param _subject [Object]
94
+ # @raise [Wallaby::NotImplemented]
95
+ def permit_params(_action, _subject)
96
+ raise NotImplemented
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallaby
4
+ # Model Decorator interface, designed to maintain metadata for all fields from the data source (database/api)
5
+ # @see Wallaby::ResourceDecorator for more information on how to customize metadata
6
+ class ModelDecorator
7
+ include Fieldable
8
+
9
+ # Initialize with model class
10
+ # @param model_class [Class]
11
+ def initialize(model_class)
12
+ @model_class = model_class
13
+ end
14
+
15
+ # @!attribute [r] model_class
16
+ # @return [Class]
17
+ attr_reader :model_class
18
+
19
+ # @!attribute [r] fields
20
+ # @note to be implemented in subclasses.
21
+ # Origin fields metadata.
22
+ #
23
+ # Initially, {#index_fields}, {#show_fields} and {#form_fields} are copies of it.
24
+ # @return [ActiveSupport::HashWithIndifferentAccess]
25
+ def fields
26
+ raise NotImplemented
27
+ end
28
+
29
+ # @!attribute [w] model_class
30
+ def fields=(fields)
31
+ @fields = fields.with_indifferent_access
32
+ end
33
+
34
+ # @!attribute [r] index_fields
35
+ # @note to be implemented in subclasses.
36
+ # Fields metadata for `index` page.
37
+ # @return [ActiveSupport::HashWithIndifferentAccess]
38
+ def index_fields
39
+ raise NotImplemented
40
+ end
41
+
42
+ # @!attribute [w] index_fields
43
+ def index_fields=(fields)
44
+ @index_fields = fields.with_indifferent_access
45
+ end
46
+
47
+ # @!attribute [w] index_field_names
48
+ attr_writer :index_field_names
49
+
50
+ # @!attribute [r] index_field_names
51
+ # A list of field names for `index` page
52
+ # @return [Array<String, Symbol>]
53
+ def index_field_names
54
+ @index_field_names ||= reposition index_fields.keys, primary_key
55
+ end
56
+
57
+ # @!attribute [r] show_fields
58
+ # @note to be implemented in subclasses.
59
+ # Fields metadata for `show` page.
60
+ # @return [ActiveSupport::HashWithIndifferentAccess]
61
+ def show_fields
62
+ raise NotImplemented
63
+ end
64
+
65
+ # @!attribute [w] show_fields
66
+ def show_fields=(fields)
67
+ @show_fields = fields.with_indifferent_access
68
+ end
69
+
70
+ # @!attribute [w] show_field_names
71
+ attr_writer :show_field_names
72
+
73
+ # @!attribute [r] show_field_names
74
+ # A list of field names for `show` page
75
+ # @return [Array<String, Symbol>]
76
+ def show_field_names
77
+ @show_field_names ||= reposition show_fields.keys, primary_key
78
+ end
79
+
80
+ # @!attribute [r] form_fields
81
+ # @note to be implemented in subclasses.
82
+ # Fields metadata for form (`new`/`edit`) page.
83
+ # @return [ActiveSupport::HashWithIndifferentAccess]
84
+ def form_fields
85
+ raise NotImplemented
86
+ end
87
+
88
+ # @!attribute [w] form_fields
89
+ def form_fields=(fields)
90
+ @form_fields = fields.with_indifferent_access
91
+ end
92
+
93
+ # @!attribute [w] form_field_names
94
+ attr_writer :form_field_names
95
+
96
+ # @!attribute [r] form_field_names
97
+ # A list of field names for form (`new`/`edit`) page
98
+ # @return [Array<String, Symbol>]
99
+ def form_field_names
100
+ @form_field_names ||= reposition form_fields.keys, primary_key
101
+ end
102
+
103
+ # @!attribute [r] filters
104
+ # @note to be implemented in subclasses.
105
+ # Filter metadata for index page.
106
+ # @return [ActiveSupport::HashWithIndifferentAccess]
107
+ def filters
108
+ @filters ||= ::ActiveSupport::HashWithIndifferentAccess.new
109
+ end
110
+
111
+ # @note to be implemented in subclasses.
112
+ # Validation error for given resource
113
+ # @param _resource [Object]
114
+ # @return [ActiveModel::Errors, Hash]
115
+ def form_active_errors(_resource)
116
+ raise NotImplemented
117
+ end
118
+
119
+ # @!attribute [w] primary_key
120
+ attr_writer :primary_key
121
+
122
+ # @!attribute [r] primary_key
123
+ # @note to be implemented in subclasses.
124
+ # @return [String] primary key name
125
+ def primary_key
126
+ raise NotImplemented
127
+ end
128
+
129
+ # @note to be implemented in subclasses.
130
+ # To guess the title for a resource.
131
+ #
132
+ # This title will be used on the following places:
133
+ #
134
+ # - page title on show page
135
+ # - in the response for autocomplete association field
136
+ # - title of show link for a resource
137
+ # @param _resource [Object]
138
+ # @return [String] title of current resource
139
+ def guess_title(_resource)
140
+ raise NotImplemented
141
+ end
142
+
143
+ # @return [String]
144
+ # @see Wallaby::Map.resources_name_map
145
+ def resources_name
146
+ Map.resources_name_map model_class
147
+ end
148
+
149
+ protected
150
+
151
+ # Move primary key to the front for given field names.
152
+ # @param field_names [Array<String, Symbol>] field names
153
+ # @param primary_key [String, Symbol] primary key name
154
+ # @return [Array<String, Symbol>]
155
+ # a new list of field names that primary key goes first
156
+ def reposition(field_names, primary_key)
157
+ field_names.unshift(primary_key.to_s).uniq
158
+ end
159
+
160
+ # Validate presence of a type for given field name
161
+ # @param type [String, Symbol, nil]
162
+ # @return [String, Symbol] type
163
+ # @raise [ArgumentError] when type is nil
164
+ def validate_presence_of(field_name, type)
165
+ type || raise(::ArgumentError, I18n.t('errors.invalid.type_required', field_name: field_name))
166
+ end
167
+ end
168
+ end