plutonium 0.13.2 → 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/views/components/breadcrumbs/breadcrumbs_component.html.erb +1 -1
- data/lib/generators/pu/pkg/app/templates/config/routes.rb.tt +5 -6
- data/lib/generators/pu/pkg/app/templates/lib/engine.rb.tt +0 -4
- data/lib/generators/pu/res/conn/conn_generator.rb +4 -4
- data/lib/plutonium/application/controller.rb +1 -1
- data/lib/plutonium/application/dynamic_controllers.rb +108 -0
- data/lib/plutonium/auth/rodauth.rb +1 -1
- data/lib/plutonium/concerns/resource_validatable.rb +34 -0
- data/lib/plutonium/core/controllers/entity_scoping.rb +84 -26
- data/lib/plutonium/pkg/app.rb +3 -115
- data/lib/plutonium/pkg/concerns/resource_validatable.rb +36 -0
- data/lib/plutonium/railtie.rb +57 -22
- data/lib/plutonium/resource/controller.rb +1 -1
- data/lib/plutonium/resource_register.rb +83 -0
- data/lib/plutonium/routing/mapper_extensions.rb +127 -0
- data/lib/plutonium/routing/resource_registration.rb +16 -0
- data/lib/plutonium/routing/route_set_extensions.rb +132 -0
- data/lib/plutonium/smart_cache.rb +151 -0
- data/lib/plutonium/version.rb +1 -1
- metadata +10 -3
- data/lib/generators/pu/rodauth/templates/app/rodauth/account_rodauth_plugin.rb.tt +0 -270
data/lib/plutonium/railtie.rb
CHANGED
@@ -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 =
|
7
|
-
config.plutonium.enable_hotreload =
|
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[
|
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
|
-
|
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
|
-
|
76
|
+
def load_base_component
|
31
77
|
load Plutonium.root.join("app", "views", "components", "base.rb")
|
32
78
|
end
|
33
79
|
|
34
|
-
|
80
|
+
def load_plutonium_initializers
|
35
81
|
Dir.glob(Plutonium.root.join("config", "initializers", "**", "*.rb")) { |file| load file }
|
36
82
|
end
|
37
83
|
|
38
|
-
|
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
|
-
|
57
|
-
|
58
|
-
|
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.
|
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
|
data/lib/plutonium/version.rb
CHANGED