active_manageable 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 (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