plutonium 0.13.2 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,45 +1,88 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "view_component"
2
4
 
3
5
  module Plutonium
6
+ # Plutonium::Railtie integrates Plutonium with Rails applications.
7
+ #
8
+ # This Railtie sets up configurations, initializers, and tasks for Plutonium
9
+ # to work seamlessly within a Rails environment.
4
10
  class Railtie < Rails::Railtie
11
+ # Configuration options for Plutonium
5
12
  config.plutonium = ActiveSupport::OrderedOptions.new
6
- config.plutonium.cache_discovery = defined?(Rails.env) && !Rails.env.development?
7
- config.plutonium.enable_hotreload = defined?(Rails.env) && Rails.env.development?
13
+ config.plutonium.cache_discovery = !Rails.env.development?
14
+ config.plutonium.enable_hotreload = Rails.env.development?
8
15
 
16
+ # Asset configuration
9
17
  config.plutonium.assets = ActiveSupport::OrderedOptions.new
10
18
  config.plutonium.assets.logo = "plutonium.png"
11
19
  config.plutonium.assets.favicon = "plutonium.ico"
12
20
  config.plutonium.assets.stylesheet = "plutonium.css"
13
21
  config.plutonium.assets.script = "plutonium.min.js"
14
22
 
23
+ # Assets to be precompiled
24
+ #
15
25
  # If you don't want to precompile Plutonium's assets (eg. because you're using webpack),
16
26
  # you can do this in an intiailzer:
17
27
  #
18
28
  # config.after_initialize do
19
29
  # config.assets.precompile -= Plutonium::Railtie::PRECOMPILE_ASSETS
20
30
  # end
21
- PRECOMPILE_ASSETS = %w[plutonium.js plutonium.js.map plutonium.min.js plutonium.min.js.map plutonium.css plutonium.png plutonium.ico]
31
+ PRECOMPILE_ASSETS = %w[
32
+ plutonium.js plutonium.js.map plutonium.min.js plutonium.min.js.map
33
+ plutonium.css plutonium.png plutonium.ico
34
+ ].freeze
22
35
 
23
36
  initializer "plutonium.assets" do
24
- next unless Rails.application.config.respond_to?(:assets)
37
+ setup_asset_pipeline if Rails.application.config.respond_to?(:assets)
38
+ end
39
+
40
+ initializer "plutonium.load_components" do
41
+ load_base_component
42
+ end
43
+
44
+ initializer "plutonium.initializers" do
45
+ load_plutonium_initializers
46
+ end
47
+
48
+ initializer "plutonium.asset_server" do
49
+ setup_development_asset_server if Plutonium.development?
50
+ end
51
+
52
+ initializer "plutonium.view_components_capture_compat" do
53
+ config.view_component.capture_compatibility_patch_enabled = true
54
+ end
55
+
56
+ initializer "plutonium.action_dispatch_extensions" do
57
+ extend_action_dispatch
58
+ end
59
+
60
+ rake_tasks do
61
+ load "tasks/create_rodauth_admin.rake"
62
+ end
25
63
 
64
+ config.after_initialize do
65
+ Plutonium::Reloader.start! if Rails.application.config.plutonium.enable_hotreload
66
+ Plutonium::ZEITWERK_LOADER.eager_load if Rails.env.production?
67
+ end
68
+
69
+ private
70
+
71
+ def setup_asset_pipeline
26
72
  Rails.application.config.assets.precompile += PRECOMPILE_ASSETS
27
73
  Rails.application.config.assets.paths << Plutonium.root.join("app/assets").to_s
28
74
  end
29
75
 
30
- initializer "plutonium.load_components" do
76
+ def load_base_component
31
77
  load Plutonium.root.join("app", "views", "components", "base.rb")
32
78
  end
33
79
 
34
- initializer "plutonium.initializers" do
80
+ def load_plutonium_initializers
35
81
  Dir.glob(Plutonium.root.join("config", "initializers", "**", "*.rb")) { |file| load file }
36
82
  end
37
83
 
38
- initializer "plutonium.asset_server" do
39
- next unless Plutonium.development?
40
-
84
+ def setup_development_asset_server
41
85
  puts "=> [plutonium] starting assets server"
42
- # setup a middleware to serve our assets
43
86
  config.app_middleware.insert_before(
44
87
  ActionDispatch::Static,
45
88
  Rack::Static,
@@ -47,23 +90,15 @@ module Plutonium
47
90
  root: Plutonium.root.join("src").to_s,
48
91
  cascade: true,
49
92
  header_rules: [
50
- # Cache all static files in public caches (e.g. Rack::Cache) as well as in the browser
51
93
  [:all, {"cache-control" => "public, max-age=31536000"}]
52
94
  ]
53
95
  )
54
96
  end
55
97
 
56
- initializer "plutonium.view_components_capture_compat" do
57
- config.view_component.capture_compatibility_patch_enabled = true
58
- end
59
-
60
- rake_tasks do
61
- load "tasks/create_rodauth_admin.rake"
62
- end
63
-
64
- config.after_initialize do
65
- Plutonium::Reloader.start! if Rails.application.config.plutonium.enable_hotreload
66
- Plutonium::ZEITWERK_LOADER.eager_load if Rails.env.production?
98
+ def extend_action_dispatch
99
+ ActionDispatch::Routing::Mapper.prepend Plutonium::Routing::MapperExtensions
100
+ ActionDispatch::Routing::RouteSet.prepend Plutonium::Routing::RouteSetExtensions
101
+ Rails::Engine.include Plutonium::Routing::ResourceRegistration
67
102
  end
68
103
  end
69
104
  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.2"
2
+ VERSION = "0.14.0"
3
3
  end