plutonium 0.13.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 866175a8ec5a15285d1fe65350957749b173840f32ed54bfd4eeb929d4ecc735
4
- data.tar.gz: 759aff94626c9b5eb9851d0dba4f4fb3f9221284d00bafd960fe95eae87181b7
3
+ metadata.gz: 2a68eccc72cf54155fa9d9959a22b60d13ea434a4faf34221ec0314e5ab74124
4
+ data.tar.gz: 99c748e56b3b07eb7361f67887137430eef9cb35a3e0b3bd04c1de831ae68b04
5
5
  SHA512:
6
- metadata.gz: 7f51b28186a4f9618657426c4e63a99eac77483f814fdd471cd05193c2d90dececc208ebd73464565b70d923101889f40655a17303b9e248292695af646f8e92
7
- data.tar.gz: d72197c60fa506326f3888c1f1afedc4c0bf09302399b5a55ababee52210b7220c732eeaeb167c94444d94d658b38ea30350c6ee609723f625814b3b3f4a3ace
6
+ metadata.gz: 6ada4ff77eddc1daa695bd4ff96542f5f72b378ab7826c99f256faa089182c9084edd5db2433d044d6270df5714097e9e46add301631446ead9c1ae034ead0b9
7
+ data.tar.gz: e600eb51acf5873891b4c38b4541e1220c4e62f19eb74b9194c68203e73e0178d3e6ef74ee9437daf9dd27ced14b93ebe2807d3f4338b160e3de26dddd21691b
@@ -59,7 +59,7 @@
59
59
  <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4"/>
60
60
  </svg>
61
61
  <span class="ms-1 text-sm font-medium text-gray-500 md:ms-2 dark:text-gray-200">
62
- <%= resource.persisted? ? display_name_of(resource) : 'New' %>
62
+ <%= resource.persisted? ? display_name_of(resource) : "Create" %>
63
63
  </span>
64
64
  </li>
65
65
  <% end %>
@@ -1,11 +1,10 @@
1
- <%= package_name %>::Engine.draw_custom_routes do
2
- # draw custom routes here
3
-
1
+ <%= package_name %>::Engine.routes.draw do
4
2
  root to: "dashboard#index"
5
- end
6
3
 
7
- # draw routes for registered resources
8
- <%= package_name %>::Engine.draw_resource_routes
4
+ # register resources above.
5
+
6
+ # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
7
+ end
9
8
 
10
9
  # mount our app
11
10
  Rails.application.routes.draw do
@@ -4,11 +4,7 @@ module <%= package_name %>
4
4
  # add concerns above.
5
5
 
6
6
  config.after_initialize do
7
- initialize_register!
8
-
9
7
  # add directives above.
10
-
11
- # register resources above.
12
8
  end
13
9
  end
14
10
  end
@@ -15,13 +15,13 @@ module Pu
15
15
 
16
16
  def start
17
17
  source_feature = select_feature msg: "Select source feature"
18
- source_module = (source_feature == "main_app") ? "ResourceRecord" : "#{source_feature.classify}::ResourceRecord"
18
+ source_module = (source_feature == "main_app") ? "ResourceRecord" : "#{source_feature.camelize}::ResourceRecord"
19
19
 
20
20
  Plutonium.eager_load_rails!
21
21
  available_resources = source_module.constantize.descendants.map(&:to_s).sort
22
22
  selected_resources = prompt.multi_select("Select resources", available_resources)
23
23
 
24
- @app_namespace = select_app.classify
24
+ @app_namespace = select_app.camelize
25
25
 
26
26
  selected_resources.each do |resource|
27
27
  @resource_class = resource
@@ -31,8 +31,8 @@ module Pu
31
31
  # template "app/presenters/resource_presenter.rb", "packages/#{package_namespace}/app/presenters/#{package_namespace}/#{resource.underscore}_presenter.rb"
32
32
  # template "app/query_objects/resource_query_object.rb", "packages/#{package_namespace}/app/query_objects/#{package_namespace}/#{resource.underscore}_query_object.rb"
33
33
 
34
- insert_into_file "packages/#{package_namespace}/lib/engine.rb",
35
- indent("register_resource ::#{resource}\n", 6),
34
+ insert_into_file "packages/#{package_namespace}/config/routes.rb",
35
+ indent("register_resource ::#{resource}\n", 2),
36
36
  before: /.*# register resources above.*/
37
37
  end
38
38
  rescue => e
@@ -36,7 +36,7 @@ module Plutonium
36
36
  # end
37
37
 
38
38
  def registered_resources
39
- current_engine.resource_register
39
+ current_engine.resource_register.resources
40
40
  end
41
41
  end
42
42
  end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ module Application
5
+ # DynamicControllers module provides functionality for dynamically creating controller classes
6
+ # when they are missing in the current module's namespace.
7
+ #
8
+ # @example Usage
9
+ # module MyApp
10
+ # include Plutonium::Application::DynamicControllers
11
+ # end
12
+ #
13
+ # # Now, MyApp::SomeController will be dynamically created if it doesn't exist,
14
+ # # inheriting from ::SomeController and including MyApp::Concerns::Controller
15
+ #
16
+ # @note This module is designed to be included in a parent module that represents
17
+ # a namespace for controllers.
18
+ module DynamicControllers
19
+ extend ActiveSupport::Concern
20
+
21
+ class_methods do
22
+ # Handles missing constant lookup, specifically for controller classes
23
+ #
24
+ # @param const_name [Symbol] The name of the missing constant
25
+ # @return [Class, nil] The dynamically created controller class if applicable, otherwise nil
26
+ # @raise [NameError] If the constant is not a controller and cannot be found
27
+ def const_missing(const_name)
28
+ if const_name.to_s.end_with?("Controller")
29
+ create_dynamic_controller(const_name)
30
+ else
31
+ super
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ # Creates a dynamic controller class
38
+ #
39
+ # @param const_name [Symbol] The name of the controller class to create
40
+ # @return [Class] The newly created controller class
41
+ # @raise [NameError] If the parent controller or concerns module cannot be found
42
+ def create_dynamic_controller(const_name)
43
+ parent_controller = "::#{const_name}".constantize
44
+ current_module = name
45
+ const_full_name = "#{current_module}::#{const_name}"
46
+
47
+ klass = Class.new(parent_controller) do
48
+ # YARD documentation for the dynamically created controller
49
+ # @!parse
50
+ # class DynamicController < ParentController
51
+ # include ParentModule::Concerns::Controller
52
+ # # Dynamically created controller for handling actions in the parent module
53
+ # #
54
+ # # This controller is created at runtime to handle requests within the parent namespace.
55
+ # # It inherits from the corresponding top-level controller (e.g., ::ClientsController for ParentModule::ClientsController).
56
+ # # It also includes ParentModule::Concerns::Controller.
57
+ # #
58
+ # # @note This class is created dynamically and may not have explicit method definitions.
59
+ # end
60
+ end
61
+
62
+ # Define the constant in the global namespace
63
+ define_nested_constant(const_full_name, klass)
64
+
65
+ # Include required modules
66
+ concerns_module = "#{current_module}::Concerns::Controller".constantize
67
+ route_helpers_module = "#{AdminApp}::Engine".constantize.routes.url_helpers
68
+ klass.include route_helpers_module
69
+ klass.include concerns_module
70
+
71
+ # # Run the load hooks to include necessary modules and configurations
72
+ # ActiveSupport.run_load_hooks(:action_controller, klass)
73
+
74
+ log_controller_creation(const_full_name, parent_controller)
75
+ const_full_name.constantize
76
+ rescue => e
77
+ Plutonium.logger.error "[plutonium] Failed to create dynamic controller: #{e.message}"
78
+ raise
79
+ end
80
+
81
+ # Defines a constant in the global namespace, handling nested modules
82
+ #
83
+ # @param const_full_name [String] The full module name
84
+ # @param value [Object] The value to assign to the constant
85
+ def define_nested_constant(const_full_name, value)
86
+ names = const_full_name.split("::")
87
+ const_name = names.pop
88
+
89
+ names.inject(Object) do |mod, name|
90
+ if mod.const_defined?(name)
91
+ mod.const_get(name)
92
+ else
93
+ mod.const_set(name, Module.new)
94
+ end
95
+ end.const_set(const_name, value)
96
+ end
97
+
98
+ # Logs the creation of a dynamic controller
99
+ #
100
+ # @param const_full_name [String] The full name of the created controller
101
+ # @param parent_controller [Class] The parent controller class
102
+ def log_controller_creation(const_full_name, parent_controller)
103
+ Plutonium.logger.info "[plutonium] Dynamically created #{const_full_name} < #{parent_controller}"
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -28,7 +28,7 @@ module Plutonium
28
28
  end
29
29
 
30
30
  define_singleton_method(:to_s) { "Plutonium::Auth::Rodauth(:#{name})" }
31
- define_singleton_method(:inspect) { "Plutonium::Auth::Rodautht(:#{name})" }
31
+ define_singleton_method(:inspect) { "Plutonium::Auth::Rodauth(:#{name})" }
32
32
  RUBY
33
33
  mod
34
34
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ module Concerns
5
+ # Provides methods for validating Plutonium resources
6
+ module ResourceValidatable
7
+ extend ActiveSupport::Concern
8
+
9
+ # Custom error class for invalid resources
10
+ class InvalidResourceError < StandardError; end
11
+
12
+ private
13
+
14
+ # Validates if a given resource is a valid Plutonium::Resource::Record
15
+ #
16
+ # @param resource [Object] The resource to validate
17
+ # @raise [InvalidResourceError] If the resource is not valid
18
+ # @return [void]
19
+ def validate_resource!(resource)
20
+ unless valid_resource?(resource)
21
+ raise InvalidResourceError, "#{resource} is not a valid Plutonium::Resource::Record"
22
+ end
23
+ end
24
+
25
+ # Checks if a given resource is a valid Plutonium::Resource::Record
26
+ #
27
+ # @param resource [Object] The resource to check
28
+ # @return [Boolean] True if the resource is valid, false otherwise
29
+ def valid_resource?(resource)
30
+ resource.is_a?(Class) && resource.include?(Plutonium::Resource::Record)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,6 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Plutonium
2
4
  module Core
3
5
  module Controllers
6
+ # EntityScoping module provides functionality for scoping controllers to specific entities.
7
+ #
8
+ # This module is designed to be included in controllers that need to operate within the context
9
+ # of a specific entity, such as a user's organization or a project.
10
+ #
11
+ # @example Usage in a controller
12
+ # class MyController < ApplicationController
13
+ # include Plutonium::Core::Controllers::EntityScoping
14
+ # end
4
15
  module EntityScoping
5
16
  extend ActiveSupport::Concern
6
17
 
@@ -9,70 +20,117 @@ module Plutonium
9
20
  helper_method :current_scoped_entity
10
21
  end
11
22
 
12
- private
13
-
23
+ # Checks if the current engine is scoped to an entity.
24
+ #
25
+ # @return [Boolean] true if scoped to an entity, false otherwise
14
26
  def scoped_to_entity?
15
27
  current_engine.scoped_to_entity?
16
28
  end
17
29
 
30
+ # Returns the strategy used for entity scoping.
31
+ #
32
+ # @return [Symbol] the scoping strategy
18
33
  def scoped_entity_strategy
19
34
  current_engine.scoped_entity_strategy
20
35
  end
21
36
 
22
- def scoped_entity_class
23
- ensure_legal_entity_scoping_method_access! :scoped_entity_class
37
+ # Returns the parameter key used for entity scoping.
38
+ #
39
+ # @return [Symbol] the parameter key
40
+ # @raise [NotImplementedError] if not scoped to an entity
41
+ def scoped_entity_param_key
42
+ ensure_legal_entity_scoping_method_access!(__method__)
43
+ current_engine.scoped_entity_param_key
44
+ end
24
45
 
46
+ # Returns the class of the scoped entity.
47
+ #
48
+ # @return [Class] the scoped entity class
49
+ # @raise [NotImplementedError] if not scoped to an entity
50
+ def scoped_entity_class
51
+ ensure_legal_entity_scoping_method_access!(__method__)
25
52
  current_engine.scoped_entity_class
26
53
  end
27
54
 
28
- def scoped_entity_param_key
29
- ensure_legal_entity_scoping_method_access! :scoped_entity_param_key
30
-
31
- current_engine.scoped_entity_param_key
32
- end
55
+ private
33
56
 
57
+ # Returns the session key used to store the scoped entity.
58
+ #
59
+ # @return [Symbol] the session key
60
+ # @raise [NotImplementedError] if not scoped to an entity
34
61
  def scoped_entity_session_key
35
- ensure_legal_entity_scoping_method_access! :scoped_entity_session_key
36
-
62
+ ensure_legal_entity_scoping_method_access!(__method__)
37
63
  :"#{current_package.name.underscore}__scoped_entity_id"
38
64
  end
39
65
 
66
+ # Returns the current scoped entity for the request.
67
+ #
68
+ # @return [ActiveRecord::Base, nil] the current scoped entity or nil if not found
69
+ # @raise [NotImplementedError] if not scoped to an entity or strategy is unknown
40
70
  def current_scoped_entity
41
- ensure_legal_entity_scoping_method_access! :current_scoped_entity
42
-
71
+ ensure_legal_entity_scoping_method_access!(__method__)
43
72
  return unless current_user.present?
44
73
 
45
- @current_scoped_entity ||= case scoped_entity_strategy
74
+ @current_scoped_entity ||= fetch_current_scoped_entity
75
+ end
76
+
77
+ # Fetches the current scoped entity based on the scoping strategy.
78
+ #
79
+ # @return [ActiveRecord::Base, nil] the current scoped entity or nil if not found
80
+ # @raise [NotImplementedError] if the scoping strategy is unknown
81
+ def fetch_current_scoped_entity
82
+ case scoped_entity_strategy
46
83
  when :path
47
- scoped_entity_class
48
- .associated_with(current_user)
49
- .from_path_param(request.path_parameters[scoped_entity_param_key])
50
- .first! # Raise NotFound if user does not have access to the entity or it does not exist
84
+ fetch_entity_from_path
51
85
  when Symbol
52
- send scoped_entity_strategy
86
+ send(scoped_entity_strategy)
53
87
  else
54
- raise NotImplementedError, "unknown scoped entity strategy: #{scoped_entity_strategy.inspect}"
88
+ raise NotImplementedError, "Unknown scoped entity strategy: #{scoped_entity_strategy.inspect}"
55
89
  end
56
90
  end
57
91
 
92
+ # Fetches the scoped entity from the path parameters.
93
+ #
94
+ # @return [ActiveRecord::Base] the scoped entity
95
+ # @raise [ActiveRecord::RecordNotFound] if the entity is not found or the user doesn't have access
96
+ def fetch_entity_from_path
97
+ scoped_entity_class
98
+ .associated_with(current_user)
99
+ .from_path_param(request.path_parameters[scoped_entity_param_key])
100
+ .first!
101
+ end
102
+
103
+ # Remembers the current scoped entity in the session.
104
+ #
105
+ # @return [void]
58
106
  def remember_scoped_entity
59
107
  return unless scoped_to_entity?
60
108
 
61
109
  session[scoped_entity_session_key] = current_scoped_entity.to_global_id.to_s
62
110
  end
63
111
 
112
+ # Retrieves the remembered scoped entity from the session.
113
+ #
114
+ # @return [ActiveRecord::Base, nil] the remembered scoped entity or nil if not found
115
+ # @raise [NotImplementedError] if not scoped to an entity
64
116
  def remembered_scoped_entity
65
- ensure_legal_entity_scoping_method_access! :remembered_scoped_entity
66
-
67
- @remembered_scoped_entity ||= GlobalID::Locator.locate session[scoped_entity_session_key]
117
+ ensure_legal_entity_scoping_method_access!(__method__)
118
+ @remembered_scoped_entity ||= GlobalID::Locator.locate(session[scoped_entity_session_key])
68
119
  end
69
120
 
121
+ # Ensures that the method call is legal within the current scoping context.
122
+ #
123
+ # @param method [Symbol] the method being called
124
+ # @raise [NotImplementedError] if not scoped to an entity
70
125
  def ensure_legal_entity_scoping_method_access!(method)
71
126
  return if scoped_to_entity?
72
127
 
73
- raise NotImplementedError, "this request is not scoped to an entity\n\n" \
74
- "add the `scope_to_entity YourEntityRecord` directive in " \
75
- "#{current_engine} or implement #{self.class}##{method}"
128
+ raise NotImplementedError, <<~ERROR_MESSAGE
129
+ This request is not scoped to an entity.
130
+
131
+ Add the `scope_to_entity YourEntityRecord` directive in #{current_engine}
132
+ or implement #{self.class}##{method}
133
+ ERROR_MESSAGE
76
134
  end
77
135
  end
78
136
  end
@@ -1,4 +1,4 @@
1
- require "active_support/notifications"
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Plutonium
4
4
  module Pkg
@@ -11,10 +11,11 @@ module Plutonium
11
11
  end
12
12
 
13
13
  class_methods do
14
+ include Plutonium::Concerns::ResourceValidatable
14
15
  attr_reader :scoped_entity_class, :scoped_entity_strategy, :scoped_entity_param_key
15
16
 
16
17
  def scope_to_entity(entity_class, strategy: :path, param_key: nil)
17
- raise "#{entity_class} is not a valid resource record" unless entity_class.include?(Plutonium::Resource::Record)
18
+ validate_resource! entity_class
18
19
 
19
20
  @scoped_entity_class = entity_class
20
21
  @scoped_entity_strategy = strategy
@@ -25,122 +26,9 @@ module Plutonium
25
26
  scoped_entity_class.present?
26
27
  end
27
28
 
28
- def initialize_register!
29
- # this exists solely to support hot reloads
30
- # if the user has modified the register especially if they removed a registration, we have no way of telling
31
- # so instead we start over
32
- @resource_register = []
33
- end
34
-
35
- def register_resource(resource)
36
- @resource_register.append resource
37
- end
38
-
39
- def resource_register
40
- @resource_register || []
41
- end
42
-
43
- def registered_resource_route_key_lookup
44
- @registered_resource_route_key_lookup = resource_register.map { |resource|
45
- [resource.model_name.singular_route_key.to_sym, resource]
46
- }.to_h
47
- end
48
-
49
- def draw_custom_routes(&block)
50
- @custom_routes_block = block
51
- end
52
-
53
- def draw_resource_routes
54
- ActiveSupport::Notifications.instrument("plutonium.app.draw_resource_routes", app: self.class.module_parent.to_s) do
55
- draw_resource_routes_internal
56
- end
57
- end
58
-
59
29
  def dom_id
60
30
  module_parent_name.underscore.dasherize
61
31
  end
62
-
63
- private
64
-
65
- def draw_resource_routes_internal
66
- custom_routes_block = @custom_routes_block
67
- registered_resources = resource_register
68
- scoped_entity_param_key = self.scoped_entity_param_key if scoped_entity_strategy == :path
69
- routes.draw do
70
- shared_resource_concerns = [:interactive_resource_actions] # TODO: make this a config parameter
71
- concern :interactive_resource_actions do
72
- # these concerns power the interactive actions feature
73
- member do
74
- get "record_actions/:interactive_action", action: :begin_interactive_resource_record_action,
75
- as: :interactive_resource_record_action
76
- post "record_actions/:interactive_action", action: :commit_interactive_resource_record_action
77
- end
78
-
79
- collection do
80
- get "collection_actions/:interactive_action", action: :begin_interactive_resource_collection_action,
81
- as: :interactive_resource_collection_action
82
- post "collection_actions/:interactive_action", action: :commit_interactive_resource_collection_action
83
-
84
- get "recordless_actions/:interactive_action", action: :begin_interactive_resource_recordless_action,
85
- as: :interactive_resource_recordless_action
86
- post "recordless_actions/:interactive_action", action: :commit_interactive_resource_recordless_action
87
- end
88
- end
89
-
90
- resource_route_names = []
91
- resource_route_opts_lookup = {}
92
- # for each of our registered resources, we are registering the routes required
93
- registered_resources.each do |resource|
94
- resource_name = resource.to_s # Deeply::Namespaced::ResourceModel
95
- resource_controller = resource_name.pluralize.underscore # deeply/namespaced/resource_models
96
- resource_route = resource.model_name.plural # deeply_namespaced_resource_models
97
- resource_route_name = :"#{resource_route}_routes" # deeply_namespaced_resource_models_routes
98
-
99
- resource_route_opts = {}
100
- # rails is not smart enough to infer Deeply::Namespaced::ResourceModelsController from deeply_namespaced_resource_models
101
- # since we are heavy on namespaces, we choose to be explicit to guarantee there is no confusion
102
- resource_route_opts[:controller] = resource_controller
103
- # using collection for path is much nicer than the alternative
104
- # e.g. deeply/namespaced/resource_models vs deeply_namespaced_resource_models
105
- resource_route_opts[:path] = resource.model_name.collection
106
- resource_route_opts_lookup[resource_route] = resource_route_opts
107
-
108
- # defining our resources with concerns allows us to defer materializing till later,
109
- # ensuring that resource_route_opts_lookup is populated
110
- concern resource_route_name do
111
- resources resource_route, **resource_route_opts, concerns: shared_resource_concerns do
112
- nested_resources_route_opts = resource_route_opts_lookup.slice(*resource.has_many_association_routes)
113
- nested_resources_route_opts.each do |nested_resource_route, nested_resource_route_opts|
114
- resources nested_resource_route, **nested_resource_route_opts, concerns: shared_resource_concerns
115
- end
116
- end
117
- end
118
- resource_route_names << resource_route_name
119
- end
120
-
121
- # materialize our routes using a scope
122
- # if the app is scoped to an entity, ensure that the expected route param and url helper prefix are specified.
123
-
124
- # path => /:entity/deeply/namespaced/resource_models/:deeply_namespaced_resource_model_id/
125
- # helper => entity_deeply_namespaced_resource_models_path
126
- scope_name = scoped_entity_param_key.present? ? ":#{scoped_entity_param_key}" : ""
127
-
128
- # path => /deeply/namespaced/resource_models/:deeply_namespaced_resource_model_id/
129
- # helper => deeply_namespaced_resource_models_path
130
- scope_options = scoped_entity_param_key.present? ? {as: scoped_entity_param_key} : {}
131
-
132
- scope scope_name, scope_options do
133
- instance_exec(&custom_routes_block) if custom_routes_block.present?
134
- # we have to reverse sort our resource routes in order to prevent routing conflicts
135
- # e.g. /blogs/1 and blogs/comments cause an issue if Blog is registered before Blogs::Comment
136
- # attempting to load blogs/comments routes to blogs/:id which fails with a 404 since BlogsController
137
- # essentially performs a Blog.find('comments')
138
- # since the route names for these 2 will be 'blogs' and 'blog_comments',
139
- # reverse sorting ensures that blog_comments is registered first, preventing the issue described above
140
- concerns resource_route_names.sort
141
- end
142
- end
143
- end
144
32
  end
145
33
  end
146
34
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ module Pkg
5
+ module Concerns
6
+ # Provides methods for validating Plutonium resources
7
+ module ResourceValidatable
8
+ extend ActiveSupport::Concern
9
+
10
+ # Custom error class for invalid resources
11
+ class InvalidResourceError < StandardError; end
12
+
13
+ private
14
+
15
+ # Validates if a given resource is a valid Plutonium::Resource::Record
16
+ #
17
+ # @param resource [Object] The resource to validate
18
+ # @raise [InvalidResourceError] If the resource is not valid
19
+ # @return [void]
20
+ def validate_resource!(resource)
21
+ unless valid_resource?(resource)
22
+ raise InvalidResourceError, "#{resource} is not a valid Plutonium::Resource::Record"
23
+ end
24
+ end
25
+
26
+ # Checks if a given resource is a valid Plutonium::Resource::Record
27
+ #
28
+ # @param resource [Object] The resource to check
29
+ # @return [Boolean] True if the resource is valid, false otherwise
30
+ def valid_resource?(resource)
31
+ resource.is_a?(Class) && resource.include?(Plutonium::Resource::Record)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end