plutonium 0.13.3 → 0.14.1

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/pu/core/assets/assets_generator.rb +2 -2
  3. data/lib/generators/pu/core/install/install_generator.rb +0 -3
  4. data/lib/generators/pu/core/install/templates/config/initializers/plutonium.rb +10 -0
  5. data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +19 -0
  6. data/lib/generators/pu/pkg/app/templates/config/routes.rb.tt +5 -6
  7. data/lib/generators/pu/pkg/app/templates/lib/engine.rb.tt +0 -4
  8. data/lib/generators/pu/res/conn/conn_generator.rb +4 -4
  9. data/lib/plutonium/application/controller.rb +1 -1
  10. data/lib/plutonium/application/dynamic_controllers.rb +108 -0
  11. data/lib/plutonium/auth/rodauth.rb +1 -1
  12. data/lib/plutonium/concerns/resource_validatable.rb +34 -0
  13. data/lib/plutonium/config/overlayed_hash.rb +86 -0
  14. data/lib/plutonium/configuration.rb +138 -0
  15. data/lib/plutonium/core/autodiscovery/association_renderer_discoverer.rb +1 -1
  16. data/lib/plutonium/core/autodiscovery/input_discoverer.rb +1 -1
  17. data/lib/plutonium/core/autodiscovery/renderer_discoverer.rb +1 -1
  18. data/lib/plutonium/core/controllers/entity_scoping.rb +84 -26
  19. data/lib/plutonium/helpers/assets_helper.rb +73 -20
  20. data/lib/plutonium/pkg/app.rb +3 -115
  21. data/lib/plutonium/pkg/concerns/resource_validatable.rb +36 -0
  22. data/lib/plutonium/railtie.rb +53 -30
  23. data/lib/plutonium/reloader.rb +66 -24
  24. data/lib/plutonium/resource/controller.rb +1 -1
  25. data/lib/plutonium/resource_register.rb +83 -0
  26. data/lib/plutonium/routing/mapper_extensions.rb +127 -0
  27. data/lib/plutonium/routing/resource_registration.rb +16 -0
  28. data/lib/plutonium/routing/route_set_extensions.rb +132 -0
  29. data/lib/plutonium/smart_cache.rb +151 -0
  30. data/lib/plutonium/version.rb +1 -1
  31. data/lib/plutonium.rb +41 -27
  32. data/sig/.keep +0 -0
  33. metadata +13 -4
  34. data/lib/generators/pu/rodauth/templates/app/rodauth/account_rodauth_plugin.rb.tt +0 -270
  35. data/sig/plutonium.rbs +0 -12
@@ -1,15 +1,21 @@
1
- require "active_support/notifications"
1
+ # frozen_string_literal: true
2
2
 
3
- # Be sure to restart your server when you modify this file.
3
+ require "active_support/notifications"
4
4
 
5
5
  module Plutonium
6
+ # Reloader class for Plutonium
7
+ #
8
+ # This class is responsible for managing the reloading of Plutonium components
9
+ # and related files during development.
6
10
  class Reloader
7
11
  class << self
12
+ # Start the reloader
13
+ #
14
+ # @return [void]
8
15
  def start!
9
16
  puts "=> [plutonium] starting reloader"
10
17
 
11
18
  ActiveSupport::Notifications.instrument("plutonium.reloader.start") do
12
- # Task code here
13
19
  @listener&.stop
14
20
  @listener = initialize_listener
15
21
  end
@@ -17,34 +23,46 @@ module Plutonium
17
23
 
18
24
  private
19
25
 
26
+ # Initialize the file listener
27
+ #
28
+ # @return [Listen::Listener, nil] the initialized listener or nil if no paths to watch
20
29
  def initialize_listener
21
30
  require "listen"
22
31
 
23
32
  reload_paths = gather_reload_paths
24
- return unless reload_paths.any?
33
+ return if reload_paths.empty?
25
34
 
26
- listener = Listen.to(*reload_paths, only: /\.rb$/) do |modified, added, removed|
35
+ Listen.to(*reload_paths, only: /\.rb$/) { |modified, added, removed|
27
36
  handle_file_changes(modified, added, removed)
28
- end
29
- listener.start
30
- listener
37
+ }.tap(&:start)
31
38
  end
32
39
 
40
+ # Gather paths to be watched for changes
41
+ #
42
+ # @return [Array<String>] list of paths to watch
33
43
  def gather_reload_paths
34
44
  reload_paths = []
35
45
 
36
- if Plutonium.development?
37
- reload_paths << Plutonium.lib_root.to_s
38
- reload_paths << Plutonium.root.join("app", "views", "components").to_s
39
- reload_paths << Plutonium.root.join("config", "initializers").to_s
46
+ if Plutonium.configuration.development?
47
+ reload_paths.concat([
48
+ Plutonium.lib_root.to_s,
49
+ Plutonium.root.join("app", "views", "components").to_s,
50
+ Plutonium.root.join("config", "initializers").to_s
51
+ ])
40
52
  end
41
53
 
42
- packages_dir = Rails.root.join("packages/").to_s
54
+ packages_dir = Rails.root.join("packages").to_s
43
55
  reload_paths << packages_dir if File.directory?(packages_dir)
44
56
 
45
57
  reload_paths
46
58
  end
47
59
 
60
+ # Handle file changes detected by the listener
61
+ #
62
+ # @param modified [Array<String>] list of modified files
63
+ # @param added [Array<String>] list of added files
64
+ # @param removed [Array<String>] list of removed files
65
+ # @return [void]
48
66
  def handle_file_changes(modified, added, removed)
49
67
  (modified + added).each do |file|
50
68
  Plutonium.logger.debug "[plutonium] change detected: #{file}"
@@ -62,46 +80,70 @@ module Plutonium
62
80
  end
63
81
  end
64
82
 
83
+ # Check if the file is within the packages directory
84
+ #
85
+ # @param file [String] path to the file
86
+ # @return [Boolean] true if the file is within the packages directory
65
87
  def file_starts_with_packages_dir?(file)
66
- packages_dir = Rails.root.join("packages/").to_s
67
- file.starts_with?(packages_dir)
88
+ file.start_with?(Rails.root.join("packages").to_s)
68
89
  end
69
90
 
91
+ # Handle changes to files within the packages directory
92
+ #
93
+ # @param file [String] path to the changed file
94
+ # @param added [Array<String>] list of added files
95
+ # @return [void]
70
96
  def handle_package_file_changes(file, added)
71
97
  return if added.include?(file)
72
98
 
73
99
  case File.basename(file)
74
100
  when "engine.rb"
75
101
  reload_engine_and_routes(file)
76
- else
77
- # Non-engine package files are reloaded by Rails automatically
78
102
  end
79
103
  end
80
104
 
105
+ # Reload engine and routes
106
+ #
107
+ # @param file [String] path to the engine file
108
+ # @return [void]
81
109
  def reload_engine_and_routes(file)
82
110
  Plutonium.logger.debug "[plutonium] reloading: engine+routes"
83
111
  load file
84
112
  Rails.application.reload_routes!
85
113
  end
86
114
 
115
+ # Reload the framework and file
116
+ #
117
+ # @param file [String] path to the file
118
+ # @return [void]
87
119
  def reload_framework_and_file(file)
88
- # # Ensure that the file loads correctly before we do any reloading
89
- # load file
90
-
91
120
  Plutonium.logger.debug "[plutonium] reloading: app+framework"
92
121
  Rails.application.reloader.reload!
93
122
  Plutonium::ZEITWERK_LOADER.reload
94
- # reload components
123
+ reload_components
124
+ end
125
+
126
+ # Reload components
127
+ #
128
+ # @return [void]
129
+ def reload_components
95
130
  Object.send(:remove_const, "PlutoniumUi")
96
131
  load Plutonium.root.join("app", "views", "components", "base.rb")
97
- # # Ensure files that do not contain constants are loaded again e.g. initializers
98
- # load file
99
132
  end
100
133
 
134
+ # Reload a single file
135
+ #
136
+ # @param file [String] path to the file
137
+ # @return [Boolean] true if the file was successfully loaded
101
138
  def reload_file(file)
102
- load file
139
+ load(file)
103
140
  end
104
141
 
142
+ # Log reload failure
143
+ #
144
+ # @param file [String] path to the file that failed to reload
145
+ # @param error [StandardError] the error that occurred during reloading
146
+ # @return [void]
105
147
  def log_reload_failure(file, error)
106
148
  Plutonium.logger.error "\n[plutonium] reloading failed\n\n#{error.message}\n"
107
149
  end
@@ -123,7 +123,7 @@ module Plutonium
123
123
 
124
124
  @current_parent ||= begin
125
125
  parent_route_key = parent_route_param.to_s.gsub(/_id$/, "").to_sym
126
- parent_class = current_engine.registered_resource_route_key_lookup[parent_route_key]
126
+ parent_class = current_engine.resource_register.route_key_lookup[parent_route_key]
127
127
  parent_scope = parent_class.from_path_param(params[parent_route_param])
128
128
  parent_scope = parent_scope.associated_with(current_scoped_entity) if scoped_to_entity?
129
129
  parent_scope.first!
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ # ResourceRegister manages the registration and lookup of resources.
5
+ class ResourceRegister
6
+ include Plutonium::SmartCache
7
+ include Concerns::ResourceValidatable
8
+
9
+ # Custom error class for frozen register operations
10
+ class FrozenRegisterError < StandardError; end
11
+
12
+ def initialize
13
+ @resources = Set.new
14
+ @frozen = false
15
+ end
16
+
17
+ # Registers a new resource with the register.
18
+ #
19
+ # @param resource [Class] The resource class to be registered.
20
+ # @raise [Plutonium::Concerns::ResourceValidatable::InvalidResourceError] If the resource is not a valid Plutonium::Resource::Record.
21
+ # @raise [FrozenRegisterError] If the register is frozen.
22
+ # @return [void]
23
+ def register(resource)
24
+ raise FrozenRegisterError, "Cannot modify frozen resource register" if @frozen
25
+
26
+ validate_resource!(resource)
27
+ @resources.add(resource.to_s)
28
+ end
29
+
30
+ # Returns an array of all registered resource classes and freezes the register.
31
+ #
32
+ # @return [Array<Class>] An array of registered resource classes.
33
+ def resources
34
+ freeze
35
+ @resources.map(&:constantize)
36
+ end
37
+ memoize_unless_reloading :resources
38
+
39
+ # Returns a hash mapping route keys to their corresponding resource classes.
40
+ # This method will freeze the register if it hasn't been frozen already.
41
+ #
42
+ # @return [Hash{Symbol => Class}] A hash where keys are route keys and values are resource classes.
43
+ def route_key_lookup
44
+ freeze
45
+ resources.to_h do |resource|
46
+ [resource.model_name.singular_route_key.to_sym, resource]
47
+ end
48
+ end
49
+ memoize_unless_reloading :route_key_lookup
50
+
51
+ # Clears all registered resources and invalidates the cache.
52
+ #
53
+ # @return [void]
54
+ def clear
55
+ @resources.clear
56
+ @frozen = false
57
+ invalidate_cache
58
+ end
59
+
60
+ # Checks if the register is frozen.
61
+ #
62
+ # @return [Boolean] True if the register is frozen, false otherwise.
63
+ def frozen?
64
+ @frozen
65
+ end
66
+
67
+ private
68
+
69
+ # Freezes the register
70
+ #
71
+ # @return [Boolean] Always returns true
72
+ def freeze
73
+ @frozen ||= true
74
+ end
75
+
76
+ # Invalidates the memoization cache
77
+ #
78
+ # @return [void]
79
+ def invalidate_cache
80
+ flush_smart_cache
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ module Routing
5
+ # MapperExtensions module provides additional functionality for route mapping in Plutonium applications.
6
+ #
7
+ # This module extends the functionality of Rails' routing mapper to support Plutonium-specific features,
8
+ # such as resource registration and custom route materialization.
9
+ #
10
+ # @example Usage in a Rails routes file
11
+ # Blorgh::Engine.routes.draw do
12
+ # register_resource SomeModel
13
+ # end
14
+ module MapperExtensions
15
+ # Registers a resource for routing and sets up associated routes.
16
+ #
17
+ # @param resource [Class] The resource class to be registered.
18
+ # @param options [Hash] Additional options for resource registration.
19
+ # @yield An optional block for additional resource configuration.
20
+ # @return [void]
21
+ def register_resource(resource, options = {}, &)
22
+ route_config = route_set.register_resource(resource, &)
23
+ define_resource_routes(route_config, resource)
24
+ resource_route_concern_names << route_config[:concern_name]
25
+ end
26
+
27
+ private
28
+
29
+ # @return [Array<Symbol>] Names of resource route concerns.
30
+ def resource_route_concern_names
31
+ @resource_route_concern_names ||= []
32
+ end
33
+
34
+ # Sets up shared concerns for interactive resource actions.
35
+ #
36
+ # @return [void]
37
+ def setup_shared_resource_concerns
38
+ concern :interactive_resource_actions do
39
+ define_member_interactive_actions
40
+ define_collection_interactive_actions
41
+ end
42
+ end
43
+
44
+ # Materializes all registered resource routes.
45
+ #
46
+ # @return [void]
47
+ def materialize_resource_routes
48
+ engine = route_set.engine
49
+ scope_params = determine_scope_params(engine)
50
+
51
+ scope scope_params[:name], scope_params[:options] do
52
+ concerns resource_route_concern_names.sort
53
+ end
54
+ end
55
+
56
+ # @return [ActionDispatch::Routing::RouteSet] The current route set.
57
+ def route_set
58
+ @set
59
+ end
60
+
61
+ # Defines routes for a registered resource.
62
+ #
63
+ # @param route_config [Hash] Configuration for the resource routes.
64
+ # @param resource [Class] The resource class.
65
+ # @return [void]
66
+ def define_resource_routes(route_config, resource)
67
+ concern route_config[:concern_name] do
68
+ resources route_config[:route_name], **route_config[:route_options] do
69
+ instance_exec(&route_config[:block]) if route_config[:block]
70
+ define_nested_resource_routes(resource)
71
+ end
72
+ end
73
+ end
74
+
75
+ # Defines nested resource routes for a given resource.
76
+ #
77
+ # @param resource [Class] The parent resource class.
78
+ # @return [void]
79
+ def define_nested_resource_routes(resource)
80
+ nested_configs = route_set.resource_route_config_for(*resource.has_many_association_routes)
81
+ nested_configs.each do |nested_config|
82
+ resources nested_config[:route_name], **nested_config[:route_options] do
83
+ instance_exec(&nested_config[:block]) if nested_config[:block]
84
+ end
85
+ end
86
+ end
87
+
88
+ # Defines member-level interactive actions.
89
+ #
90
+ # @return [void]
91
+ def define_member_interactive_actions
92
+ member do
93
+ get "record_actions/:interactive_action", action: :begin_interactive_resource_record_action,
94
+ as: :interactive_resource_record_action
95
+ post "record_actions/:interactive_action", action: :commit_interactive_resource_record_action
96
+ end
97
+ end
98
+
99
+ # Defines collection-level interactive actions.
100
+ #
101
+ # @return [void]
102
+ def define_collection_interactive_actions
103
+ collection do
104
+ get "collection_actions/:interactive_action", action: :begin_interactive_resource_collection_action,
105
+ as: :interactive_resource_collection_action
106
+ post "collection_actions/:interactive_action", action: :commit_interactive_resource_collection_action
107
+
108
+ get "recordless_actions/:interactive_action", action: :begin_interactive_resource_recordless_action,
109
+ as: :interactive_resource_recordless_action
110
+ post "recordless_actions/:interactive_action", action: :commit_interactive_resource_recordless_action
111
+ end
112
+ end
113
+
114
+ # Determines the scope parameters based on the engine configuration.
115
+ #
116
+ # @param engine [Class] The current engine.
117
+ # @return [Hash] Scope name and options.
118
+ def determine_scope_params(engine)
119
+ scoped_entity_param_key = engine.scoped_entity_param_key if engine.scoped_entity_strategy == :path
120
+ {
121
+ name: scoped_entity_param_key.present? ? ":#{scoped_entity_param_key}" : "",
122
+ options: scoped_entity_param_key.present? ? {as: scoped_entity_param_key} : {}
123
+ }
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ module Routing
5
+ # The ResourceRegistration module provides functionality for registering and managing resources
6
+ module ResourceRegistration
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ def resource_register
11
+ @resource_register ||= Plutonium::ResourceRegister.new
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ module Routing
5
+ # RouteSetExtensions module provides additional functionality for route management in Plutonium applications.
6
+ #
7
+ # This module extends the functionality of Rails' routing system to support Plutonium-specific features,
8
+ # such as resource registration and custom route drawing.
9
+ #
10
+ # @example Usage in a Rails application
11
+ # Blorgh::Engine.routes.draw do
12
+ # register_resource SomeModel
13
+ # end
14
+ module RouteSetExtensions
15
+ # Clears all registered resources and route configurations.
16
+ #
17
+ # This method should be called when you want to reset all registered resources
18
+ # and start with a clean slate for route definition.
19
+ #
20
+ # @return [void]
21
+ def clear!
22
+ resource_route_config_lookup.clear
23
+ engine.resource_register.clear
24
+ super
25
+ end
26
+
27
+ # Draws routes with additional Plutonium-specific setup and resource materialization.
28
+ #
29
+ # @param block [Proc] The block containing route definitions.
30
+ # @return [void]
31
+ # @yield Executes the given block in the context of route drawing.
32
+ def draw(&block)
33
+ if supported_engine?
34
+ ActiveSupport::Notifications.instrument("plutonium.resource_routes.draw", app: engine.to_s) do
35
+ super do
36
+ setup_shared_resource_concerns
37
+ instance_exec(&block)
38
+ materialize_resource_routes
39
+ end
40
+ end
41
+ else
42
+ super(&block)
43
+ end
44
+ end
45
+
46
+ # Registers a resource for routing.
47
+ #
48
+ # @param resource [Class] The resource class to be registered.
49
+ # @yield An optional block for additional resource configuration.
50
+ # @return [Hash] The configuration for the registered resource.
51
+ # @raise [ArgumentError] If the engine doesn't support Plutonium::Pkg::App.
52
+ def register_resource(resource, &)
53
+ validate_engine!
54
+ engine.resource_register.register(resource)
55
+
56
+ route_name = resource.model_name.plural
57
+ concern_name = :"#{route_name}_routes"
58
+
59
+ config = create_resource_config(resource, route_name, concern_name, &)
60
+ resource_route_config_lookup[route_name] = config
61
+
62
+ config
63
+ end
64
+
65
+ # Retrieves the route configuration for specified routes.
66
+ #
67
+ # @param routes [Array<Symbol>] The route names to fetch configurations for.
68
+ # @return [Array<Hash>] An array of route configurations.
69
+ def resource_route_config_for(*routes)
70
+ routes = Array(routes)
71
+ resource_route_config_lookup.slice(*routes).values
72
+ end
73
+
74
+ # Returns the current engine for the routes.
75
+ #
76
+ # @return [Class] The engine class (Rails application or custom engine).
77
+ def engine
78
+ @engine ||= determine_engine
79
+ end
80
+
81
+ private
82
+
83
+ # @return [Hash] A lookup table for resource route configurations.
84
+ def resource_route_config_lookup
85
+ @resource_route_config_lookup ||= {}
86
+ end
87
+
88
+ # Validates that the current engine supports Plutonium features.
89
+ #
90
+ # @raise [ArgumentError] If the engine doesn't include Plutonium::Pkg::App.
91
+ # @return [void]
92
+ def validate_engine!
93
+ raise ArgumentError, "#{engine} must include Plutonium::Pkg::App to register resources" unless supported_engine?
94
+ end
95
+
96
+ # Checks if the current engine supports Plutonium features.
97
+ #
98
+ # @return [Boolean] True if the engine includes Plutonium::Pkg::App, false otherwise.
99
+ def supported_engine?
100
+ engine.include?(Plutonium::Pkg::App)
101
+ end
102
+
103
+ # Determines the appropriate engine based on the current scope.
104
+ #
105
+ # @return [Class] The determined engine class.
106
+ def determine_engine
107
+ engine_module = default_scope&.fetch(:module)
108
+ engine_module.present? ? "#{engine_module.camelize}::Engine".constantize : Rails.application.class
109
+ end
110
+
111
+ # Creates a resource configuration hash.
112
+ #
113
+ # @param resource_name [String] The name of the resource.
114
+ # @param route_name [String] The pluralized name for routes.
115
+ # @param concern_name [Symbol] The name of the concern for this resource.
116
+ # @yield An optional block for additional resource configuration.
117
+ # @return [Hash] The complete resource configuration.
118
+ def create_resource_config(resource, route_name, concern_name, &block)
119
+ {
120
+ route_name: route_name,
121
+ concern_name: concern_name,
122
+ route_options: {
123
+ controller: resource.to_s.pluralize.underscore,
124
+ path: resource.model_name.collection,
125
+ concerns: %i[interactive_resource_actions]
126
+ },
127
+ block: block
128
+ }
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ # The SmartCache module provides flexible caching mechanisms for classes and objects,
5
+ # allowing for both inline caching and method-level memoization.
6
+ #
7
+ # This module is designed to optimize performance by caching results
8
+ # when class caching is enabled (typically in production),
9
+ # while ensuring fresh results when caching is disabled (typically in development).
10
+ #
11
+ # This implementation is thread-safe.
12
+ #
13
+ # @example Including SmartCache in a class
14
+ # class MyClass
15
+ # include Plutonium::SmartCache
16
+ #
17
+ # def my_method(arg)
18
+ # cache_unless_reloading("my_method_#{arg}") { expensive_operation(arg) }
19
+ # end
20
+ #
21
+ # def another_method(arg)
22
+ # # Method implementation
23
+ # end
24
+ # memoize_unless_reloading :another_method
25
+ # end
26
+ module SmartCache
27
+ extend ActiveSupport::Concern
28
+
29
+ included do
30
+ class_attribute :_memoized_results, instance_writer: false, default: Concurrent::Map.new
31
+ end
32
+
33
+ # Caches the result of the given block unless class caching is disabled.
34
+ #
35
+ # @param cache_key [String] A unique key to identify the cached result
36
+ # @yield The block whose result will be cached
37
+ # @return [Object] The result of the block, either freshly computed or from cache
38
+ #
39
+ # @example Using cache_unless_reloading inline
40
+ # def fetch_user_data(user_id)
41
+ # cache_unless_reloading("user_data_#{user_id}") do
42
+ # UserDataService.fetch(user_id)
43
+ # end
44
+ # end
45
+ #
46
+ # @note This method uses Rails.application.config.cache_classes
47
+ # to determine whether to cache or not. When cache_classes is false
48
+ # (typical in development), it will always yield to get a fresh result.
49
+ # When true (typical in production), it will use the cache.
50
+ def cache_unless_reloading(cache_key, &block)
51
+ return yield unless should_cache?
52
+
53
+ @cached_results ||= Concurrent::Map.new
54
+ @cached_results.compute_if_absent(cache_key) { yield }
55
+ end
56
+
57
+ # Flushes the smart cache for the specified keys or all keys if none are specified.
58
+ #
59
+ # @param keys [Array<Symbol, String>, Symbol, String] The cache key(s) to flush
60
+ # @return [void]
61
+ #
62
+ # @example Flushing specific cache keys
63
+ # flush_smart_cache([:user_data, :product_list])
64
+ #
65
+ # @example Flushing all cache keys
66
+ # flush_smart_cache
67
+ #
68
+ # @note This method clears both inline caches and memoized method results.
69
+ def flush_smart_cache(keys = nil)
70
+ keys = Array(keys).map(&:to_sym)
71
+ if keys.present?
72
+ @cached_results&.delete_if { |k, _| keys.include?(k.to_sym) }
73
+ keys.each { |key| self.class._memoized_results.delete(key) }
74
+ else
75
+ @cached_results&.clear
76
+ self.class._memoized_results.clear
77
+ end
78
+ end
79
+
80
+ # Determines whether caching should be performed based on the current Rails configuration.
81
+ #
82
+ # @return [Boolean] true if caching should be performed, false otherwise
83
+ # @note This method uses Rails.application.config.cache_classes to determine caching behavior.
84
+ # When cache_classes is false (typical in development), it returns false.
85
+ # When true (typical in production), it returns true.
86
+ def should_cache?
87
+ Rails.application.config.cache_classes
88
+ end
89
+
90
+ class_methods do
91
+ # Memoizes the result of the specified method unless class caching is disabled.
92
+ #
93
+ # @param method_name [Symbol] The name of the method to memoize
94
+ # @return [void]
95
+ #
96
+ # @example Memoizing a method
97
+ # class User
98
+ # include Plutonium::SmartCache
99
+ #
100
+ # def expensive_full_name_calculation
101
+ # # Complex name calculation
102
+ # end
103
+ # memoize_unless_reloading :expensive_full_name_calculation
104
+ # end
105
+ #
106
+ # @note This method uses Rails.application.config.cache_classes to determine
107
+ # whether to memoize or not. When cache_classes is false (typical in development),
108
+ # it will always call the original method. When true (typical in production),
109
+ # it will use memoization, caching results for each unique set of arguments.
110
+ def memoize_unless_reloading(method_name)
111
+ original_method = instance_method(method_name)
112
+ define_method(method_name) do |*args|
113
+ if should_cache?
114
+ cache = self.class._memoized_results[method_name] ||= Concurrent::Map.new
115
+ cache.compute_if_absent(args.hash.to_s) { original_method.bind_call(self, *args) }
116
+ else
117
+ original_method.bind_call(self, *args)
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ # Configuration:
125
+ # The caching behavior is controlled by the Rails configuration option config.cache_classes:
126
+ #
127
+ # - When false (typical in development):
128
+ # - Classes are reloaded on each request.
129
+ # - cache_unless_reloading always yields fresh results.
130
+ # - memoize_unless_reloading always calls the original method.
131
+ #
132
+ # - When true (typical in production):
133
+ # - Classes are cached.
134
+ # - cache_unless_reloading uses cached results.
135
+ # - memoize_unless_reloading uses memoized results, caching for each unique set of arguments.
136
+ #
137
+ # Best Practices:
138
+ # - Use meaningful and unique cache keys to avoid collisions.
139
+ # - Be mindful of memory usage, especially with large cached results.
140
+ # - Consider cache expiration strategies for long-running processes.
141
+ # - Use cache_unless_reloading for fine-grained control within methods.
142
+ # - Use memoize_unless_reloading for entire methods, especially those with expensive computations.
143
+ #
144
+ # Thread Safety:
145
+ # - This implementation is thread-safe.
146
+ # - It uses Concurrent::Map from the concurrent-ruby gem for thread-safe caching.
147
+ #
148
+ # Testing:
149
+ # - In your test environment, you may want to control caching behavior explicitly.
150
+ # - You can mock or stub Rails.application.config.cache_classes or override should_cache? as needed in your tests.
151
+ end
@@ -1,3 +1,3 @@
1
1
  module Plutonium
2
- VERSION = "0.13.3"
2
+ VERSION = "0.14.1"
3
3
  end