active_manageable 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +52 -0
  3. data/.gitignore +20 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +42 -0
  6. data/.rubocop_rails.yml +201 -0
  7. data/.rubocop_rspec.yml +68 -0
  8. data/.standard.yml +5 -0
  9. data/Appraisals +27 -0
  10. data/CHANGELOG.md +5 -0
  11. data/CODE_OF_CONDUCT.md +84 -0
  12. data/Gemfile +6 -0
  13. data/Gemfile.lock +194 -0
  14. data/LICENSE.txt +21 -0
  15. data/README.md +758 -0
  16. data/Rakefile +8 -0
  17. data/active_manageable.gemspec +75 -0
  18. data/bin/console +15 -0
  19. data/bin/setup +8 -0
  20. data/gemfiles/.bundle/config +2 -0
  21. data/gemfiles/rails_6_0.gemfile +8 -0
  22. data/gemfiles/rails_6_1.gemfile +8 -0
  23. data/gemfiles/rails_7_0.gemfile +8 -0
  24. data/lib/active_manageable/authorization/cancancan.rb +28 -0
  25. data/lib/active_manageable/authorization/pundit.rb +28 -0
  26. data/lib/active_manageable/base.rb +218 -0
  27. data/lib/active_manageable/configuration.rb +58 -0
  28. data/lib/active_manageable/methods/auxiliary/includes.rb +98 -0
  29. data/lib/active_manageable/methods/auxiliary/model_attributes.rb +59 -0
  30. data/lib/active_manageable/methods/auxiliary/order.rb +43 -0
  31. data/lib/active_manageable/methods/auxiliary/scopes.rb +59 -0
  32. data/lib/active_manageable/methods/auxiliary/select.rb +46 -0
  33. data/lib/active_manageable/methods/auxiliary/unique_search.rb +50 -0
  34. data/lib/active_manageable/methods/create.rb +20 -0
  35. data/lib/active_manageable/methods/destroy.rb +23 -0
  36. data/lib/active_manageable/methods/edit.rb +25 -0
  37. data/lib/active_manageable/methods/index.rb +49 -0
  38. data/lib/active_manageable/methods/new.rb +20 -0
  39. data/lib/active_manageable/methods/show.rb +25 -0
  40. data/lib/active_manageable/methods/update.rb +23 -0
  41. data/lib/active_manageable/pagination/kaminari.rb +39 -0
  42. data/lib/active_manageable/search/ransack.rb +38 -0
  43. data/lib/active_manageable/version.rb +5 -0
  44. data/lib/active_manageable.rb +43 -0
  45. metadata +373 -0
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ # load a file relative to the current location
4
+ require_relative "lib/active_manageable/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "active_manageable"
8
+ spec.version = ActiveManageable::VERSION
9
+ spec.authors = ["Chris Hilton", "Chris Branson"]
10
+ spec.email = ["449774+chrismhilton@users.noreply.github.com", "138595+chrisbranson@users.noreply.github.com"]
11
+
12
+ spec.summary = "Business logic framework for Ruby on Rails"
13
+ spec.description = "Framework for business logic classes in a Ruby on Rails application"
14
+ spec.homepage = "https://github.com/CircleSD/active_manageable"
15
+ spec.license = "MIT"
16
+
17
+ # Minimum version of Ruby compatible with Rails 7.0
18
+ spec.required_ruby_version = ">= 2.7.0"
19
+
20
+ # Metadata used on gem’s profile page on rubygems.org
21
+ spec.metadata["homepage_uri"] = spec.homepage
22
+ spec.metadata["source_code_uri"] = spec.homepage
23
+ spec.metadata["changelog_uri"] = "https://github.com/CircleSD/active_manageable/blob/main/CHANGELOG.md"
24
+ spec.metadata["bug_tracker_uri"] = "https://github.com/CircleSD/active_manageable/issues"
25
+ spec.metadata["documentation_uri"] = spec.homepage
26
+
27
+ # Specify which files should be added to the gem when it is released.
28
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
29
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
30
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
31
+ end
32
+
33
+ # Binary folder where the gem’s executables are located
34
+ spec.bindir = "exe"
35
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
36
+
37
+ # Add lib directory to $LOAD_PATH to make code available via the require statement
38
+ spec.require_paths = ["lib"]
39
+
40
+ # Register runtime and development dependencies
41
+ # including gems that are essential to test and build this gem
42
+
43
+ # rails dependencies
44
+ spec.add_dependency "activerecord", ">= 6.0"
45
+ spec.add_dependency "activesupport", ">= 6.0"
46
+
47
+ # gem dependencies
48
+ spec.add_dependency "rails-i18n"
49
+ spec.add_dependency "flexitime", "~> 1.0"
50
+
51
+ # test dependencies
52
+ spec.add_development_dependency "rake", "~> 13.0"
53
+ spec.add_development_dependency "appraisal"
54
+ spec.add_development_dependency "sqlite3", "~> 1.4.0"
55
+ spec.add_development_dependency "rspec-rails"
56
+ spec.add_development_dependency "rails-controller-testing"
57
+ spec.add_development_dependency "factory_bot_rails"
58
+ spec.add_development_dependency "shoulda-matchers"
59
+ spec.add_development_dependency "simplecov"
60
+
61
+ # test modules
62
+ spec.add_development_dependency "pundit"
63
+ spec.add_development_dependency "cancancan"
64
+ spec.add_development_dependency "ransack"
65
+ spec.add_development_dependency "kaminari"
66
+
67
+ # linter dependencies
68
+ spec.add_development_dependency "rubocop", "1.23.0"
69
+ spec.add_development_dependency "standard"
70
+ spec.add_development_dependency "rubocop-rails"
71
+ spec.add_development_dependency "rubocop-rspec"
72
+
73
+ # For more information and examples about making a new gem, checkout our
74
+ # guide at: https://bundler.io/guides/creating_gem.html
75
+ end
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "active_manageable"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,2 @@
1
+ ---
2
+ BUNDLE_RETRY: "1"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 6.0.1"
6
+ gem "activesupport", "~> 6.0.1"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 6.1"
6
+ gem "activesupport", "~> 6.1"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 7.0"
6
+ gem "activesupport", "~> 7.0"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,28 @@
1
+ #
2
+ # Authorization methods for CanCanCan
3
+ # https://github.com/CanCanCommunity/cancancan
4
+ #
5
+ module ActiveManageable
6
+ module Authorization
7
+ module CanCanCan
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ private
12
+
13
+ def authorize(record:, action: nil)
14
+ action ||= @current_method
15
+ current_ability.authorize!(action, record)
16
+ end
17
+
18
+ def scoped_class
19
+ model_class.accessible_by(current_ability)
20
+ end
21
+
22
+ def current_ability
23
+ ::Ability.new(current_user)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ #
2
+ # Authorization methods for Pundit
3
+ # https://github.com/varvet/pundit
4
+ #
5
+ module ActiveManageable
6
+ module Authorization
7
+ module Pundit
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ private
12
+
13
+ def authorize(record:, action: nil)
14
+ action ||= authorize_action
15
+ ::Pundit.authorize(current_user, record, action)
16
+ end
17
+
18
+ def scoped_class
19
+ ::Pundit.policy_scope(current_user, model_class)
20
+ end
21
+
22
+ def authorize_action
23
+ "#{@current_method}?".to_sym
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,218 @@
1
+ module ActiveManageable
2
+ class Base
3
+ class_attribute :model_class, instance_writer: false, instance_predicate: false
4
+ class_attribute :defaults, instance_writer: false, instance_predicate: false
5
+ class_attribute :module_initialize_state_methods, instance_writer: false, instance_predicate: false
6
+
7
+ attr_reader :target, :current_method, :attributes, :options
8
+
9
+ # target provides a common variable to use within the CRUD, auxiliary & library methods
10
+ # whereas the object & collection methods provide less ambiguous external access
11
+ alias_method :object, :target
12
+ alias_method :collection, :target
13
+
14
+ class << self
15
+ # Ruby method called when a child class inherits from a parent class
16
+ def inherited(subclass)
17
+ super
18
+ # necessary to set default value here rather than in class_attribute declaration
19
+ # otherwise all subclasses share the same hash/array instance
20
+ subclass.defaults = {}
21
+ subclass.module_initialize_state_methods = []
22
+ end
23
+
24
+ delegate :current_user, to: ActiveManageable
25
+
26
+ # Include the required action methods in your class
27
+ # either all methods using the ActiveManageable::ALL_METHODS constant
28
+ # or selective methods from :index, :show, :new, :create, :edit, :update, :destroy
29
+ # and optionally set the :model_class
30
+ #
31
+ # For example:-
32
+ # manageable ActiveManageable::ALL_METHODS
33
+ # manageable ActiveManageable::ALL_METHODS, model_class: Album
34
+ # manageable :index, :show
35
+ def manageable(*methods)
36
+ options = methods.extract_options!.dup
37
+
38
+ methods = ActiveManageable::Methods.constants if methods[0] == ActiveManageable::ALL_METHODS
39
+
40
+ methods.each do |method|
41
+ include ActiveManageable::Methods.const_get(method.to_s.classify)
42
+ end
43
+
44
+ include_authorization
45
+ include_search
46
+ include_pagination
47
+
48
+ options.each { |key, value| send(:"#{key}=", value) }
49
+
50
+ set_model_class
51
+ end
52
+
53
+ private
54
+
55
+ def include_authorization
56
+ case ActiveManageable.configuration.authorization_library
57
+ when :pundit
58
+ include ActiveManageable::Authorization::Pundit
59
+ when :cancancan
60
+ include ActiveManageable::Authorization::CanCanCan
61
+ end
62
+ end
63
+
64
+ def include_search
65
+ case ActiveManageable.configuration.search_library
66
+ when :ransack
67
+ include ActiveManageable::Search::Ransack
68
+ end
69
+ end
70
+
71
+ def include_pagination
72
+ case ActiveManageable.configuration.pagination_library
73
+ when :kaminari
74
+ include ActiveManageable::Pagination::Kaminari
75
+ end
76
+ end
77
+
78
+ def set_model_class
79
+ self.model_class ||= begin
80
+ if name.end_with?(ActiveManageable.configuration.subclass_suffix)
81
+ name.chomp(ActiveManageable.configuration.subclass_suffix).classify.constantize
82
+ end
83
+ rescue NameError
84
+ end
85
+ end
86
+
87
+ def initialize_state_methods(*methods)
88
+ module_initialize_state_methods.concat(methods)
89
+ end
90
+
91
+ def add_method_defaults(key:, value:, methods:)
92
+ methods = Array(methods).map(&:to_sym)
93
+ methods << :all if methods.empty?
94
+ defaults[key] ||= {}
95
+ methods.each { |method| defaults[key][method] = value }
96
+ end
97
+ end
98
+
99
+ def current_user
100
+ @current_user || ActiveManageable.current_user
101
+ end
102
+
103
+ def with_current_user(user)
104
+ @current_user = user
105
+ yield
106
+ ensure
107
+ @current_user = nil
108
+ end
109
+
110
+ private
111
+
112
+ def initialize_state(attributes: {}, options: {})
113
+ @target = nil
114
+ @current_method = calling_method
115
+ @attributes = normalize_attributes(state_argument_to_hwia(attributes))
116
+ @options = state_argument_to_hwia(options)
117
+ module_initialize_state_methods.each { |method| send(method) }
118
+ end
119
+
120
+ def authorize(record:, action: nil)
121
+ end
122
+
123
+ # Returns the name of the calling method
124
+ # using caller_locations that returns the current execution stack
125
+ # and catering for inheritance and two occurrences of initialize_state in the execution stack
126
+ # when the ransack module is included as it has its own definition of the method that calls super
127
+ # https://www.lucascaton.com.br/2016/11/04/ruby-how-to-get-the-name-of-the-calling-method
128
+ def calling_method
129
+ my_caller = caller_locations(1..1).first.label
130
+ caller_locations[1..].find { |location| location.label != my_caller }.label.to_sym
131
+ end
132
+
133
+ # Converts a state argument to a ActiveSupport::HashWithIndifferentAccess.
134
+ # The purpose of the method is to return a duplicate object for arguments like the attributes
135
+ # so that any changes that are made internally do not affect the source object.
136
+ #
137
+ # For a Hash returns an ActiveSupport::HashWithIndifferentAccess.
138
+ # For a ActiveSupport::HashWithIndifferentAccess returns a duplicate ActiveSupport::HashWithIndifferentAccess.
139
+ # For an ActionController::Parameters returns a safe ActiveSupport::HashWithIndifferentAccess
140
+ # representation of the parameters with all unpermitted keys removed.
141
+ #
142
+ # NB: For an ActionController::Parameters we experimented with using the deep_dup method
143
+ # to return a duplicate ActionController::Parameters but this caused issues
144
+ # for the attributes argument when the params had been permiited and then nested params are replaced with a hash.
145
+ # That would be fine if the attributes were then used for mass assignment, however,
146
+ # first we call normalize_attribute_values which uses the each method
147
+ # which converts all hashes into ActionController::Parameters with the permitted attribute set to false
148
+ # so when performing mass assignment that resulted in a ActiveModel::ForbiddenAttributesError.
149
+ def state_argument_to_hwia(arg)
150
+ case arg
151
+ when Hash
152
+ arg.with_indifferent_access
153
+ when action_controller_params?
154
+ arg.to_h
155
+ else
156
+ arg
157
+ end
158
+ end
159
+
160
+ # Returns true if the object is an ActionController::Parameters
161
+ # using class.name so the gem does not need a dependency on ActionPack simply for a case statement
162
+ def action_controller_params?
163
+ ->(object) { object.class.name == "ActionController::Parameters" }
164
+ end
165
+
166
+ def normalize_attributes(attributes)
167
+ normalize_attribute_values(model_class, attributes)
168
+ end
169
+
170
+ # Parse date & datetime attribute values and normalize decimal separator for numeric attributes
171
+ def normalize_attribute_values(model_class, attributes)
172
+ case attributes
173
+ when Hash
174
+ attributes.each do |key, value|
175
+ if key.end_with?("_attributes")
176
+ association_class = model_association_class(model_class, key.delete_suffix("_attributes"))
177
+ normalize_attribute_values(association_class, value) if association_class.present?
178
+ else
179
+ case model_attribute_type(model_class, key)
180
+ when :date
181
+ attributes[key] = Flexitime.parse(value).try(:to_date) || value
182
+ when :datetime
183
+ attributes[key] = Flexitime.parse(value) || value
184
+ when :decimal, :float
185
+ attributes[key] = normalize_decimal_separator(value)
186
+ end
187
+ end
188
+ end
189
+ when Array
190
+ attributes.each { |attrs| normalize_attribute_values(model_class, attrs) }
191
+ end
192
+ end
193
+
194
+ def model_association_class(model_class, association_name)
195
+ model_class.reflect_on_association(association_name).try(:class_name).try(:constantize)
196
+ end
197
+
198
+ # Returns the type of the attribute with the given name
199
+ # and could be overridden by a child class in order to
200
+ # return the type of both column and non-column based attributes
201
+ # when a non-column based attribute value needed to be normalized
202
+ def model_attribute_type(model_class, attribute_name)
203
+ model_class.type_for_attribute(attribute_name).type
204
+ end
205
+
206
+ # Returns a value with a comma separator replaced with a point separator
207
+ def normalize_decimal_separator(value)
208
+ normalize_decimal_separator?(value) ? value.tr(",", ".") : value
209
+ end
210
+
211
+ # Normalize decimal separator when the locale number separator is a comma
212
+ # and the value does not include a point and includes only one comma
213
+ def normalize_decimal_separator?(value)
214
+ I18n.t("number.format.separator") == "," &&
215
+ value.to_s.count(".") == 0 && value.to_s.count(",") == 1
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,58 @@
1
+ module ActiveManageable
2
+ AUTHORIZATION_LIBRARIES = %i[pundit cancancan].freeze
3
+ SEARCH_LIBRARIES = %i[ransack].freeze
4
+ PAGINATION_LIBRARIES = %i[kaminari].freeze
5
+ LOADING_METHODS = %i[includes preload eager_load].freeze
6
+
7
+ class Configuration
8
+ attr_reader :authorization_library, :search_library, :pagination_library, :default_loading_method
9
+ attr_accessor :subclass_suffix
10
+
11
+ def initialize
12
+ @default_loading_method = :includes
13
+ @subclass_suffix = "Manager"
14
+ end
15
+
16
+ def authorization_library=(authorization_library)
17
+ raise ArgumentError.new("Invalid authorization library") unless authorization_library_valid?(authorization_library)
18
+ @authorization_library = authorization_library.to_sym
19
+ end
20
+
21
+ def search_library=(search_library)
22
+ raise ArgumentError.new("Invalid search library") unless search_library_valid?(search_library)
23
+ @search_library = search_library.to_sym
24
+ end
25
+
26
+ def pagination_library=(pagination_library)
27
+ raise ArgumentError.new("Invalid pagination library") unless pagination_library_valid?(pagination_library)
28
+ @pagination_library = pagination_library.to_sym
29
+ end
30
+
31
+ def default_loading_method=(default_loading_method)
32
+ raise ArgumentError.new("Invalid method for eager loading") unless default_loading_method_valid?(default_loading_method)
33
+ @default_loading_method = default_loading_method.to_sym
34
+ end
35
+
36
+ private
37
+
38
+ def authorization_library_valid?(authorization_library)
39
+ option_valid?(AUTHORIZATION_LIBRARIES, authorization_library)
40
+ end
41
+
42
+ def search_library_valid?(search_library)
43
+ option_valid?(SEARCH_LIBRARIES, search_library)
44
+ end
45
+
46
+ def pagination_library_valid?(pagination_library)
47
+ option_valid?(PAGINATION_LIBRARIES, pagination_library)
48
+ end
49
+
50
+ def default_loading_method_valid?(default_loading_method)
51
+ option_valid?(LOADING_METHODS, default_loading_method)
52
+ end
53
+
54
+ def option_valid?(options, option)
55
+ option.present? && options.include?(option.to_s.to_sym)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,98 @@
1
+ module ActiveManageable
2
+ module Methods
3
+ module Auxiliary
4
+ module Includes
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ # Sets the default associations to eager load when fetching records
9
+ # in the index, show, edit, update and destroy methods
10
+ # if the methods :options argument does not contain a :includes key;
11
+ # accepting a single, array or hash of association names;
12
+ # optional :methods in which to eager load the associations;
13
+ # and optional :loading_method if this needs to be different to the configuration :default_loading_method
14
+ # also accepting a lambda/proc to execute to return association names and optional :methods
15
+ #
16
+ # For example:-
17
+ # default_includes :songs
18
+ # default_includes :songs, :artist, methods: :show
19
+ # default_includes songs: :artist, loading_method: :eager_load, methods: [:index, :edit]
20
+ # default_includes -> { includes_associations }
21
+ # default_includes -> { includes_associations }, methods: [:index, :edit]
22
+ def default_includes(*associations)
23
+ options = extract_includes_options!(associations)
24
+ loading_method = options[:loading_method] || ActiveManageable.configuration.default_loading_method
25
+ assoc = associations.first.is_a?(Proc) ? associations.first : associations
26
+ value = {associations: assoc, loading_method: loading_method}
27
+ add_method_defaults(key: :includes, value: value, methods: options[:methods])
28
+ end
29
+
30
+ private
31
+
32
+ # As the associations argument may contain a single hash containing both
33
+ # the associations and options we cannot use the array extract_options! method
34
+ # as this removes the last element in the array if it's a hash.
35
+ #
36
+ # For example :-
37
+ # default_includes songs: :artist, loading_method: :preload, methods: :index
38
+ # results in an argument value of :-
39
+ # [{:songs=>:artist, :loading_method=>:preload, :methods=>:index}]
40
+ #
41
+ # So instead this method, like extract_options!, ascertains
42
+ # if the last element in the array is a hash but then it extracts
43
+ # the specific options and only removes the last element if it's empty.
44
+ # Therefore catering for the variety of association formats
45
+ # and potential presence of the options eg.
46
+ #
47
+ # [:songs, :artist, {:loading_method=>:preload, :methods=>"index"}]
48
+ # [:artist, {:songs=>:artist}, {:loading_method=>:preload, :methods=>:index}]
49
+ # [{:songs=>:artist, :loading_method=>:preload, :methods=>:index}]
50
+ def extract_includes_options!(associations)
51
+ if associations.last.is_a?(Hash)
52
+ options = associations.last.extract!(:loading_method, :methods)
53
+ associations.pop if associations.last.empty?
54
+ options
55
+ else
56
+ {}
57
+ end
58
+ end
59
+ end
60
+
61
+ included do
62
+ private
63
+
64
+ # Accepts either an array/hash of associations
65
+ # or a hash with associations and loading_method keys
66
+ # so it's possible to specify loading_method on a per request basis.
67
+ # Uses associations and loading_method from opts
68
+ # or defaults for the method or defaults for all methods
69
+ # or configuration default loading_method.
70
+ def includes(opts)
71
+ unless opts.is_a?(Hash) && opts.key?(:associations)
72
+ opts = {associations: opts}
73
+ end
74
+ associations = opts[:associations] || get_default_includes_associations
75
+ if associations.present?
76
+ loading_method = opts[:loading_method] || get_default_includes_loading_method
77
+ @target = @target.send(loading_method, associations)
78
+ else
79
+ @target
80
+ end
81
+ end
82
+
83
+ def get_default_includes_associations
84
+ includes = defaults[:includes] || {}
85
+ associations = includes.dig(@current_method, :associations) || includes.dig(:all, :associations)
86
+ associations.is_a?(Proc) ? instance_exec(&associations) : associations
87
+ end
88
+
89
+ def get_default_includes_loading_method
90
+ includes = defaults[:includes] || {}
91
+ loading_method = includes.dig(@current_method, :loading_method) || includes.dig(:all, :loading_method)
92
+ loading_method || ActiveManageable.configuration.default_loading_method
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,59 @@
1
+ module ActiveManageable
2
+ module Methods
3
+ module Auxiliary
4
+ module ModelAttributes
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ # Sets the default attribute values to use when building a model object
9
+ # in the new and create methods and these defaults are combined with
10
+ # the attribute values from the methods :attributes argument;
11
+ # accepting either a hash of attribute values or a lambda/proc
12
+ # to execute to return a hash of attribute values;
13
+ # and optional :methods in which to use the attribute values.
14
+ #
15
+ # For example:-
16
+ # default_attribute_values genre: "pop"
17
+ # default_attribute_values genre: "pop", released_at: Date.current, methods: :new
18
+ # default_attribute_values -> { default_attrs }
19
+ # default_attribute_values -> { default_attrs }, methods: [:new, :create]
20
+ def default_attribute_values(*attributes)
21
+ case attributes.first
22
+ when Hash
23
+ # when the argument contains a hash - extract the methods from the hash
24
+ # attributes value [{:name=>"Dark Side of the Moon", :genre=>"rock", :methods=>:create}]
25
+ methods = attributes.first.delete(:methods)
26
+ when Proc
27
+ # when the argument contains a lambda/proc - extract the methods from the array
28
+ # attributes value [#<Proc:0x0000000110174c90 ... (lambda)>, {:methods=>:create}]
29
+ methods = attributes.extract_options![:methods]
30
+ end
31
+ attrs = attributes.first
32
+ add_method_defaults(key: :attributes, value: attrs, methods: methods)
33
+ end
34
+ end
35
+
36
+ included do
37
+ private
38
+
39
+ # Returns attribute values to use in the new and create methods
40
+ # consisting of a merge of the method attributes argument
41
+ # and class defaults with the method argument taking precedence
42
+ def attribute_values
43
+ @attributes.is_a?(Hash) ? @attributes.reverse_merge(get_default_attribute_values) : @attributes
44
+ end
45
+
46
+ # Get the default attribute values for the method
47
+ # from the class attribute that can contain a hash of attribute values
48
+ # or a lambda/proc to execute to return attribute values
49
+ def get_default_attribute_values
50
+ default_attributes = defaults[:attributes] || {}
51
+ attributes = default_attributes[@current_method] || default_attributes[:all] || {}
52
+ attributes = (instance_exec(&attributes) || {}) if attributes.is_a?(Proc)
53
+ attributes.with_indifferent_access
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,43 @@
1
+ module ActiveManageable
2
+ module Methods
3
+ module Auxiliary
4
+ module Order
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ # Sets the default order to use when fetching records in the index method
9
+ # if the index :options argument does not contain an :order key;
10
+ # accepting attributes in the same formats as the ActiveRecord order method
11
+ # or a lambda/proc to execute to return attributes in the recognised formats.
12
+ #
13
+ # For example:-
14
+ # default_order :name
15
+ # default_order "name DESC"
16
+ # default_order -> { order_attributes }
17
+ def default_order(*attributes)
18
+ defaults[:order] = attributes.first.is_a?(Proc) ? attributes.first : attributes
19
+ end
20
+ end
21
+
22
+ included do
23
+ private
24
+
25
+ def order(attributes)
26
+ @target = @target.order(get_order_attributes(attributes))
27
+ end
28
+
29
+ def get_order_attributes(attributes)
30
+ attributes || get_default_order_attributes
31
+ end
32
+
33
+ # Get the default order attributes from the class attribute
34
+ # that can contain an array of attribute names or name & direction strings
35
+ # or a lambda/proc to execute to return an array of attribute names
36
+ def get_default_order_attributes
37
+ defaults[:order].is_a?(Proc) ? instance_exec(&defaults[:order]) : defaults[:order]
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end