plutonium 0.13.3 → 0.14.1

Sign up to get free protection for your applications and to get access to all the features.
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