plutonium 0.14.0 → 0.15.0.pre.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. checksums.yaml +4 -4
  2. data/README copy.md +1 -1
  3. data/README.md +1 -1
  4. data/app/assets/plutonium.css +1 -1
  5. data/app/views/{application → plutonium}/_resource_header.html copy.erb +1 -1
  6. data/app/views/{application → plutonium}/_resource_header.html.erb +1 -1
  7. data/app/views/{application → plutonium}/_resource_sidebar.html.erb +2 -0
  8. data/app/views/resource/_resource_details.html.erb +1 -36
  9. data/app/views/resource/_resource_form.html.erb +1 -5
  10. data/app/views/resource/_resource_table.html.erb +315 -85
  11. data/app/views/resource/edit.html.erb +1 -5
  12. data/app/views/resource/index.html.erb +1 -5
  13. data/app/views/resource/new.html.erb +1 -5
  14. data/app/views/resource/show.html.erb +1 -5
  15. data/config/initializers/pagy.rb +1 -0
  16. data/config/initializers/rabl.rb +27 -20
  17. data/gemfiles/rails_7.gemfile.lock +5 -1
  18. data/lib/generators/pu/core/assets/assets_generator.rb +2 -2
  19. data/lib/generators/pu/core/install/install_generator.rb +0 -3
  20. data/lib/generators/pu/core/install/templates/app/controllers/plutonium_controller.rb.tt +2 -0
  21. data/lib/generators/pu/core/install/templates/app/controllers/resource_controller.rb.tt +21 -1
  22. data/lib/generators/pu/core/install/templates/app/definitions/resource_definition.rb.tt +2 -0
  23. data/lib/generators/pu/core/install/templates/app/models/resource_record.rb.tt +0 -2
  24. data/lib/generators/pu/core/install/templates/config/initializers/plutonium.rb +5 -2
  25. data/lib/generators/pu/eject/shell/shell_generator.rb +2 -2
  26. data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +19 -0
  27. data/lib/generators/pu/lib/plutonium_generators/concerns/logger.rb +1 -1
  28. data/lib/generators/pu/lib/plutonium_generators/generator.rb +5 -3
  29. data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +26 -2
  30. data/lib/generators/pu/pkg/{feature/feature_generator.rb → package/package_generator.rb} +4 -4
  31. data/lib/generators/pu/pkg/{feature → package}/templates/app/controllers/resource_controller.rb.tt +0 -2
  32. data/lib/generators/pu/pkg/package/templates/app/definitions/resource_definition.rb.tt +4 -0
  33. data/lib/generators/pu/pkg/package/templates/app/query_objects/resource_query_object.rb.tt +4 -0
  34. data/lib/generators/pu/pkg/{app/app_generator.rb → portal/portal_generator.rb} +10 -8
  35. data/lib/generators/pu/pkg/{app → portal}/templates/app/controllers/concerns/controller.rb.tt +3 -7
  36. data/lib/generators/pu/pkg/{app → portal}/templates/app/controllers/dashboard_controller.rb.tt +1 -1
  37. data/lib/generators/pu/pkg/portal/templates/app/controllers/plutonium_controller.rb.tt +5 -0
  38. data/lib/generators/pu/pkg/{app/templates/app/controllers/controller.rb.tt → portal/templates/app/controllers/resource_controller.rb.tt} +1 -1
  39. data/lib/generators/pu/pkg/portal/templates/app/definitions/resource_definition.rb.tt +4 -0
  40. data/lib/generators/pu/pkg/{app → portal}/templates/app/views/package/dashboard/index.html.erb +2 -1
  41. data/lib/generators/pu/res/conn/conn_generator.rb +78 -3
  42. data/lib/generators/pu/res/conn/templates/app/controllers/resource_controller.rb.tt +1 -1
  43. data/lib/generators/pu/res/conn/templates/app/definitions/resource_definition.rb.tt +3 -0
  44. data/lib/generators/pu/res/conn/templates/app/policies/resource_policy.rb.tt +29 -1
  45. data/lib/generators/pu/res/conn/templates/app/presenters/resource_presenter.rb.tt +1 -1
  46. data/lib/generators/pu/res/conn/templates/app/query_objects/resource_query_object.rb.tt +1 -1
  47. data/lib/generators/pu/res/model/model_generator.rb +0 -7
  48. data/lib/generators/pu/res/model/templates/model.rb.tt +4 -1
  49. data/lib/generators/pu/res/scaffold/scaffold_generator.rb +22 -4
  50. data/lib/generators/pu/res/scaffold/templates/controller.rb.tt +0 -1
  51. data/lib/generators/pu/res/scaffold/templates/definition.rb.tt +4 -0
  52. data/lib/generators/pu/res/scaffold/templates/policy.rb.tt +2 -2
  53. data/lib/generators/pu/rodauth/templates/app/controllers/rodauth_controller.rb.tt +1 -1
  54. data/lib/generators/pu/rodauth/templates/app/rodauth/account_rodauth_plugin.rb.tt +270 -0
  55. data/lib/plutonium/action/README.md +0 -0
  56. data/lib/plutonium/action/base.rb +103 -0
  57. data/lib/plutonium/action/interactive.rb +117 -0
  58. data/lib/plutonium/action/route_options.rb +65 -0
  59. data/lib/plutonium/action/simple.rb +8 -0
  60. data/lib/plutonium/auth.rb +1 -1
  61. data/lib/plutonium/configuration.rb +130 -0
  62. data/lib/plutonium/core/actions/collection.rb +1 -1
  63. data/lib/plutonium/core/associations/renderers/factory.rb +3 -1
  64. data/lib/plutonium/core/autodiscovery/association_renderer_discoverer.rb +1 -1
  65. data/lib/plutonium/core/autodiscovery/input_discoverer.rb +1 -1
  66. data/lib/plutonium/core/autodiscovery/renderer_discoverer.rb +1 -1
  67. data/lib/plutonium/core/controller.rb +110 -0
  68. data/lib/plutonium/core/controllers/authorizable.rb +12 -35
  69. data/lib/plutonium/core/controllers/bootable.rb +38 -7
  70. data/lib/plutonium/core/controllers/entity_scoping.rb +6 -2
  71. data/lib/plutonium/core/fields/renderers/association_renderer.rb +1 -1
  72. data/lib/plutonium/core/ui/collection.rb +1 -1
  73. data/lib/plutonium/core/ui/detail.rb +1 -1
  74. data/lib/plutonium/core/ui/form.rb +1 -1
  75. data/lib/plutonium/definition/actions.rb +50 -0
  76. data/lib/plutonium/definition/base.rb +92 -0
  77. data/lib/plutonium/definition/config_attr.rb +30 -0
  78. data/lib/plutonium/definition/defineable_props.rb +96 -0
  79. data/lib/plutonium/definition/search.rb +21 -0
  80. data/lib/plutonium/engine/validator.rb +30 -0
  81. data/lib/plutonium/engine.rb +25 -0
  82. data/lib/plutonium/helpers/assets_helper.rb +73 -20
  83. data/lib/plutonium/helpers/form_helper.rb +1 -3
  84. data/lib/plutonium/interaction/README.md +369 -0
  85. data/lib/plutonium/interaction/base.rb +75 -0
  86. data/lib/plutonium/interaction/concerns/presentable.rb +61 -0
  87. data/lib/plutonium/interaction/concerns/workflow_dsl.rb +82 -0
  88. data/lib/plutonium/interaction/outcome.rb +129 -0
  89. data/lib/plutonium/interaction/response/base.rb +63 -0
  90. data/lib/plutonium/interaction/response/null.rb +33 -0
  91. data/lib/plutonium/interaction/response/redirect.rb +30 -0
  92. data/lib/plutonium/interaction/response/render.rb +28 -0
  93. data/lib/plutonium/lib/bit_flags.rb +70 -9
  94. data/lib/plutonium/lib/overlayed_hash.rb +86 -0
  95. data/lib/plutonium/lib/smart_cache.rb +171 -0
  96. data/lib/plutonium/models/has_cents.rb +170 -0
  97. data/lib/plutonium/{pkg/base.rb → package/engine.rb} +10 -2
  98. data/lib/plutonium/{application → portal}/controller.rb +3 -11
  99. data/lib/plutonium/{application → portal}/dynamic_controllers.rb +4 -4
  100. data/lib/plutonium/portal/engine.rb +15 -0
  101. data/lib/plutonium/railtie.rb +35 -15
  102. data/lib/plutonium/reloader.rb +71 -29
  103. data/lib/plutonium/resource/controller.rb +51 -34
  104. data/lib/plutonium/resource/controllers/authorizable.rb +128 -0
  105. data/lib/plutonium/{core → resource}/controllers/crud_actions.rb +23 -22
  106. data/lib/plutonium/resource/controllers/defineable.rb +26 -0
  107. data/lib/plutonium/{core → resource}/controllers/interactive_actions.rb +12 -12
  108. data/lib/plutonium/resource/controllers/presentable.rb +41 -0
  109. data/lib/plutonium/resource/controllers/queryable.rb +44 -0
  110. data/lib/plutonium/resource/definition.rb +6 -0
  111. data/lib/plutonium/resource/policy.rb +25 -13
  112. data/lib/plutonium/resource/query_object.rb +50 -51
  113. data/lib/plutonium/resource/record.rb +6 -89
  114. data/lib/plutonium/resource/register.rb +82 -0
  115. data/lib/plutonium/routing/mapper_extensions.rb +1 -1
  116. data/lib/plutonium/routing/resource_registration.rb +1 -1
  117. data/lib/plutonium/routing/route_set_extensions.rb +6 -18
  118. data/lib/plutonium/ui/action_button.rb +125 -0
  119. data/lib/plutonium/ui/breadcrumbs.rb +163 -0
  120. data/lib/plutonium/ui/component/base.rb +13 -0
  121. data/lib/plutonium/ui/component/behaviour.rb +38 -0
  122. data/lib/plutonium/ui/component/kit.rb +31 -0
  123. data/lib/plutonium/ui/component/methods.rb +54 -0
  124. data/lib/plutonium/ui/display/base.rb +25 -0
  125. data/lib/plutonium/ui/display/component/association.rb +26 -0
  126. data/lib/plutonium/ui/display/resource.rb +77 -0
  127. data/lib/plutonium/ui/display/theme.rb +27 -0
  128. data/lib/plutonium/ui/dyna_frame/content.rb +20 -0
  129. data/lib/plutonium/ui/empty_card.rb +20 -0
  130. data/lib/plutonium/ui/form/base.rb +37 -0
  131. data/lib/plutonium/ui/form/resource.rb +75 -0
  132. data/lib/plutonium/ui/form/theme.rb +42 -0
  133. data/lib/plutonium/ui/page/base.rb +112 -0
  134. data/lib/plutonium/ui/page/edit.rb +23 -0
  135. data/lib/plutonium/ui/page/index.rb +27 -0
  136. data/lib/plutonium/ui/page/new.rb +23 -0
  137. data/lib/plutonium/ui/page/show.rb +27 -0
  138. data/lib/plutonium/ui/page_header.rb +49 -0
  139. data/lib/plutonium/ui/table/base.rb +13 -0
  140. data/lib/plutonium/ui/table/components/pagy_info.rb +70 -0
  141. data/lib/plutonium/ui/table/components/pagy_page_info.rb +70 -0
  142. data/lib/plutonium/ui/table/components/pagy_pagination.rb +105 -0
  143. data/lib/plutonium/ui/table/components/scopes_bar.rb +136 -0
  144. data/lib/plutonium/ui/table/components/search_bar.rb +158 -0
  145. data/lib/plutonium/ui/table/display_theme.rb +21 -0
  146. data/lib/plutonium/ui/table/resource.rb +98 -0
  147. data/lib/plutonium/ui/table/theme.rb +35 -0
  148. data/lib/plutonium/ui.rb +9 -0
  149. data/lib/plutonium/version.rb +5 -1
  150. data/lib/plutonium.rb +53 -26
  151. data/package-lock.json +19 -22
  152. data/package.json +4 -4
  153. data/sig/.keep +0 -0
  154. data/src/css/plutonium.css +15 -0
  155. data/tailwind.options.js +11 -3
  156. metadata +220 -81
  157. data/lib/generators/pu/core/install/templates/app/presenters/resource_presenter.rb.tt +0 -2
  158. data/lib/generators/pu/core/install/templates/app/query_objects/resource_query_object.rb.tt +0 -2
  159. data/lib/generators/pu/pkg/feature/templates/app/query_objects/resource_query_object.rb.tt +0 -4
  160. data/lib/plutonium/concerns/resource_validatable.rb +0 -34
  161. data/lib/plutonium/config.rb +0 -9
  162. data/lib/plutonium/core/controllers/base.rb +0 -101
  163. data/lib/plutonium/core/controllers/presentable.rb +0 -65
  164. data/lib/plutonium/core/controllers/queryable.rb +0 -28
  165. data/lib/plutonium/pkg/app.rb +0 -35
  166. data/lib/plutonium/pkg/concerns/resource_validatable.rb +0 -36
  167. data/lib/plutonium/pkg/feature.rb +0 -18
  168. data/lib/plutonium/policy/initializer.rb +0 -22
  169. data/lib/plutonium/policy/scope.rb +0 -19
  170. data/lib/plutonium/pundit/context.rb +0 -18
  171. data/lib/plutonium/pundit/policy_finder.rb +0 -25
  172. data/lib/plutonium/resource/policy_context.rb +0 -5
  173. data/lib/plutonium/resource_register.rb +0 -83
  174. data/lib/plutonium/smart_cache.rb +0 -151
  175. data/sig/plutonium.rbs +0 -12
  176. /data/app/views/{application → plutonium}/_flash.html.erb +0 -0
  177. /data/app/views/{application → plutonium}/_flash_alerts.html.erb +0 -0
  178. /data/app/views/{application → plutonium}/_flash_toasts.html.erb +0 -0
  179. /data/lib/generators/pu/pkg/{app/templates/app/views/package → package/templates}/.keep +0 -0
  180. /data/lib/generators/pu/pkg/{feature → package}/templates/app/interactions/resource_interaction.rb.tt +0 -0
  181. /data/lib/generators/pu/pkg/{feature → package}/templates/app/models/resource_record.rb.tt +0 -0
  182. /data/lib/generators/pu/pkg/{feature → package}/templates/app/policies/resource_policy.rb.tt +0 -0
  183. /data/lib/generators/pu/pkg/{feature → package}/templates/app/presenters/resource_presenter.rb.tt +0 -0
  184. /data/lib/generators/pu/pkg/{feature → package}/templates/lib/engine.rb.tt +0 -0
  185. /data/lib/generators/pu/pkg/{app → portal}/templates/app/policies/resource_policy.rb.tt +0 -0
  186. /data/lib/generators/pu/pkg/{app → portal}/templates/app/presenters/resource_presenter.rb.tt +0 -0
  187. /data/lib/generators/pu/pkg/{app → portal}/templates/app/query_objects/resource_query_object.rb.tt +0 -0
  188. /data/lib/generators/pu/pkg/{feature/templates → portal/templates/app/views/package}/.keep +0 -0
  189. /data/lib/generators/pu/pkg/{app → portal}/templates/config/routes.rb.tt +0 -0
  190. /data/lib/generators/pu/pkg/{app → portal}/templates/lib/engine.rb.tt +0 -0
@@ -0,0 +1,75 @@
1
+ require "active_model"
2
+
3
+ module Plutonium
4
+ module Interaction
5
+ # Base class for all interactions.
6
+ # Provides core functionality for validations, execution, and result handling.
7
+ #
8
+ # @example
9
+ # class MyInteraction < Plutonium::Interaction::Base
10
+ # attribute :user_id, :integer
11
+ # validates :user_id, presence: true
12
+ #
13
+ # private
14
+ #
15
+ # def execute
16
+ # user = User.find(user_id)
17
+ # success(user)
18
+ # end
19
+ # end
20
+ #
21
+ # @note Subclasses must implement the #execute method.
22
+ class Base
23
+ include ActiveModel::Model
24
+ include ActiveModel::Attributes
25
+ # include Concerns::Presentable
26
+ # include Concerns::WorkflowDSL
27
+
28
+ # Executes the interaction with the given arguments.
29
+ #
30
+ # @param args [Hash] The arguments to initialize the interaction.
31
+ # @return [Plutonium::Interaction::Outcome] The result of the interaction.
32
+ def self.call(**args)
33
+ new(**args).call
34
+ end
35
+
36
+ # Executes the interaction.
37
+ #
38
+ # @return [Plutonium::Interaction::Outcome] The result of the interaction.
39
+ def call
40
+ if valid?
41
+ execute
42
+ else
43
+ failure(errors)
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ # Implement the main logic of the interaction.
50
+ #
51
+ # @abstract Subclass and override {#execute} to implement the interaction logic.
52
+ # @return [Plutonium::Interaction::Outcome] The result of the interaction.
53
+ # @raise [NotImplementedError] If the subclass doesn't implement this method.
54
+ def execute
55
+ raise NotImplementedError, "#{self.class} must implement #execute"
56
+ end
57
+
58
+ # Creates a successful outcome.
59
+ #
60
+ # @param value [Object] The value to be wrapped in the successful outcome.
61
+ # @return [Plutonium::Interaction::Success] A successful outcome.
62
+ def success(value)
63
+ Success.new(value)
64
+ end
65
+
66
+ # Creates a failure outcome.
67
+ #
68
+ # @param errors [ActiveModel::Errors, Array<String>] The errors to be wrapped in the failure outcome.
69
+ # @return [Plutonium::Interaction::Failure] A failure outcome.
70
+ def failure(errors)
71
+ Failure.new(errors)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,61 @@
1
+ module Plutonium
2
+ module Interaction
3
+ module Concerns
4
+ # Provides presentation-related functionality for interactions.
5
+ #
6
+ # This module allows interactions to define metadata such as labels, icons,
7
+ # and descriptions, which can be used for UI generation or documentation.
8
+ #
9
+ # @example
10
+ # class MyInteraction < Plutonium::Interaction::Base
11
+ # include Plutonium::Interaction::Concerns::Presentable
12
+ #
13
+ # presents label: "My Interaction",
14
+ # icon: "star",
15
+ # description: "Does something awesome"
16
+ #
17
+ # # ... rest of the interaction
18
+ # end
19
+ module Presentable
20
+ extend ActiveSupport::Concern
21
+
22
+ included do
23
+ class_attribute :presentation_metadata, default: {}
24
+ end
25
+
26
+ class_methods do
27
+ # Defines presentation metadata for the interaction.
28
+ #
29
+ # @param options [Hash] The presentation options.
30
+ # @option options [String] :label The label for the interaction.
31
+ # @option options [String] :icon The icon for the interaction.
32
+ # @option options [String] :description The description of the interaction.
33
+ def presents(**options)
34
+ self.presentation_metadata = options
35
+ end
36
+ end
37
+
38
+ # Returns the label for the interaction.
39
+ #
40
+ # @return [String] The label defined in the presentation metadata or a default generated from the class name.
41
+ def label
42
+ self.class.presentation_metadata[:label] || self.class.name.demodulize.titleize
43
+ end
44
+
45
+ # Returns the icon for the interaction.
46
+ #
47
+ # @return [String, nil] The icon defined in the presentation metadata.
48
+ def icon
49
+ self.class.presentation_metadata[:icon]
50
+ end
51
+
52
+ # Returns the description for the interaction.
53
+ #
54
+ # @return [String, nil] The description defined in the presentation metadata.
55
+ def description
56
+ self.class.presentation_metadata[:description]
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,82 @@
1
+ module Plutonium
2
+ module Interaction
3
+ module Concerns
4
+ # DO NOT USE
5
+ #
6
+ # Provides a Domain Specific Language (DSL) for defining workflows in interactions.
7
+ #
8
+ # This module allows interactions to define a series of steps that can be executed
9
+ # in sequence, with optional conditions for each step.
10
+ #
11
+ # @example
12
+ # class MyWorkflow < Plutonium::Interaction::Base
13
+ # include Plutonium::Interaction::Concerns::WorkflowDSL
14
+ #
15
+ # workflow do
16
+ # step :validate_input, ValidateInputInteraction
17
+ # step :process_data, ProcessDataInteraction, if: ->(ctx) { ctx[:data_valid] }
18
+ # step :send_notification, SendNotificationInteraction
19
+ # end
20
+ #
21
+ # private
22
+ #
23
+ # def execute
24
+ # execute_workflow(attributes.to_h)
25
+ # end
26
+ # end
27
+ module WorkflowDSL
28
+ extend ActiveSupport::Concern
29
+
30
+ included do
31
+ class_attribute :workflow_steps, default: []
32
+ end
33
+
34
+ class_methods do
35
+ # Defines the workflow for the interaction.
36
+ #
37
+ # @yield The block where workflow steps are defined.
38
+ def workflow(&block)
39
+ WorkflowBuilder.new(self).instance_eval(&block)
40
+ end
41
+ end
42
+
43
+ # Helper class for building workflows.
44
+ class WorkflowBuilder
45
+ # @param use_case_class [Class] The interaction class where the workflow is being defined.
46
+ def initialize(use_case_class)
47
+ @use_case_class = use_case_class
48
+ end
49
+
50
+ # Adds a step to the workflow.
51
+ #
52
+ # @param name [Symbol] The name of the step.
53
+ # @param use_case [Class] The interaction class to be executed for this step.
54
+ # @param if [Proc, nil] An optional condition for executing the step.
55
+ def step(name, use_case, if: nil)
56
+ @use_case_class.workflow_steps << {
57
+ name:,
58
+ use_case:,
59
+ condition: binding.local_variable_get(:if)
60
+ }
61
+ end
62
+ end
63
+
64
+ # Executes the defined workflow.
65
+ #
66
+ # @param context [Hash] The initial context for the workflow.
67
+ # @return [Plutonium::Interaction::Outcome] The outcome of the last executed step.
68
+ def execute_workflow(context = {})
69
+ self.class.workflow_steps.reduce(Success.new(context)) do |result, step|
70
+ result.and_then do |ctx|
71
+ if step[:condition].nil? || instance_exec(ctx, &step[:condition])
72
+ step[:use_case].call(context: ctx).map { |outcome| ctx[step[:name]] = outcome }
73
+ else
74
+ Success.new(ctx)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,129 @@
1
+ module Plutonium
2
+ module Interaction
3
+ # Base class for interaction outcomes.
4
+ #
5
+ # This class provides a common interface for both successful and failed outcomes
6
+ # of an interaction.
7
+ #
8
+ # @abstract Subclass and override {#and_then}, {#map}, and {#to_response} to implement
9
+ class Outcome
10
+ # @return [Array<Array(String, Symbol)>] Messages associated with the outcome.
11
+ attr_reader :messages
12
+
13
+ # Checks if the outcome is successful.
14
+ #
15
+ # @return [Boolean] true if the outcome is a Success, false otherwise.
16
+ def success?
17
+ is_a?(Success)
18
+ end
19
+
20
+ # Checks if the outcome is a failure.
21
+ #
22
+ # @return [Boolean] true if the outcome is a Failure, false otherwise.
23
+ def failure?
24
+ is_a?(Failure)
25
+ end
26
+
27
+ # Adds a message to the outcome.
28
+ #
29
+ # @param msg [String] The message to add.
30
+ # @param type [Symbol] The type of the message (e.g., :notice, :error).
31
+ # @return [self]
32
+ def with_message(msg, type = :notice)
33
+ @messages ||= []
34
+ @messages << [msg, type]
35
+ self
36
+ end
37
+
38
+ # Sets the response for the outcome.
39
+ #
40
+ # @param response [Plutonium::Interaction::Response::Base] The response to set.
41
+ #
42
+ # @abstract
43
+ # @raise [NotImplementedError] if not implemented in subclass.
44
+ def with_response(response)
45
+ raise NotImplementedError, "#{self.class} must implement #with_response"
46
+ end
47
+
48
+ # Chains another operation to be executed if this outcome is successful.
49
+ #
50
+ # @abstract
51
+ # @raise [NotImplementedError] if not implemented in subclass.
52
+ def and_then
53
+ raise NotImplementedError, "#{self.class} must implement #and_then"
54
+ end
55
+
56
+ # Converts the outcome to a response object.
57
+ #
58
+ # @abstract
59
+ # @raise [NotImplementedError] if not implemented in subclass.
60
+ def to_response
61
+ raise NotImplementedError, "#{self.class} must implement #to_response"
62
+ end
63
+ end
64
+
65
+ # Represents a successful outcome of an interaction.
66
+ class Success < Outcome
67
+ # @return [Object] The value wrapped by this successful outcome.
68
+ attr_reader :value
69
+
70
+ # @param value [Object] The value to be wrapped in this successful outcome.
71
+ def initialize(value)
72
+ @value = value
73
+ end
74
+
75
+ # Chains another operation to be executed with the value of this outcome.
76
+ #
77
+ # @yield [Object] The value wrapped by this outcome.
78
+ # @return [Outcome] The result of the yielded block.
79
+ def and_then
80
+ yield value
81
+ end
82
+
83
+ # Sets the response for this successful outcome.
84
+ #
85
+ # @param response [Plutonium::Interaction::Response::Base] The response to set.
86
+ # @return [self]
87
+ def with_response(response)
88
+ @to_response = nil
89
+ @response = response
90
+ self
91
+ end
92
+
93
+ # Converts this successful outcome to a response object.
94
+ #
95
+ # @return [Plutonium::Interaction::Response::Base] The response object.
96
+ def to_response
97
+ @to_response ||= begin
98
+ @response ||= Response::Null.new(value)
99
+ @response.with_flash(messages)
100
+ end
101
+ end
102
+ end
103
+
104
+ # Represents a failed outcome of an interaction.
105
+ class Failure < Outcome
106
+ # @return [ActiveModel::Errors, Array<String>] The errors associated with this failure.
107
+ attr_reader :errors
108
+
109
+ # @param errors [ActiveModel::Errors, Array<String>] The errors to be wrapped in this failure.
110
+ def initialize(errors)
111
+ @errors = errors
112
+ end
113
+
114
+ # Returns self without executing the given block, propagating the failure.
115
+ #
116
+ # @return [self]
117
+ def and_then
118
+ self
119
+ end
120
+
121
+ # Returns self without setting a response.
122
+ #
123
+ # @return [self]
124
+ def with_response(response)
125
+ self
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,63 @@
1
+ module Plutonium
2
+ module Interaction
3
+ module Response
4
+ # Base class for interaction responses.
5
+ #
6
+ # This class provides common functionality for handling flash messages
7
+ # and processing responses in controllers.
8
+ #
9
+ # @abstract Subclass and override {#execute} to implement
10
+ # specific response behavior.
11
+ class Base
12
+ # @return [Array<Array(String, Symbol)>] Flash messages associated with the response.
13
+ attr_reader :flash
14
+
15
+ # Initializes a new Response::Base instance.
16
+ def initialize
17
+ @flash = []
18
+ end
19
+
20
+ # Processes the response in the context of a controller.
21
+ #
22
+ # @param controller [ActionController::Base] The controller instance.
23
+ # @yield [Object] Executed if the response doesn't handle its own rendering.
24
+ # @return [void]
25
+ def process(controller, &)
26
+ set_flash(controller)
27
+ execute(controller, &)
28
+ end
29
+
30
+ # Adds flash messages to the response.
31
+ #
32
+ # @param messages [Array<Array(String, Symbol)>] The messages to add.
33
+ # @return [self]
34
+ def with_flash(messages)
35
+ @flash.concat(messages) unless messages.blank?
36
+ self
37
+ end
38
+
39
+ private
40
+
41
+ # Sets flash messages in the controller.
42
+ #
43
+ # @param controller [ActionController::Base] The controller instance.
44
+ # @return [void]
45
+ def set_flash(controller)
46
+ @flash.each do |message, type|
47
+ controller.flash[type] = message
48
+ end
49
+ end
50
+
51
+ # Executes the response logic.
52
+ #
53
+ # @abstract
54
+ # @param controller [ActionController::Base] The controller instance.
55
+ # @yield [Object] Executed if the response doesn't handle its own rendering.
56
+ # @raise [NotImplementedError] if not implemented in subclass.
57
+ def execute(controller, &)
58
+ raise NotImplementedError, "#{self.class} must implement #execute"
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,33 @@
1
+ module Plutonium
2
+ module Interaction
3
+ module Response
4
+ # Represents a null response, which doesn't perform any specific action.
5
+ #
6
+ # This class is used when an interaction doesn't produce a specific response
7
+ # type but still needs to wrap a result value.
8
+ class Null < Base
9
+ # @return [Object] The result value wrapped by this null response.
10
+ attr_reader :result
11
+
12
+ # Initializes a new Null response.
13
+ #
14
+ # @param result [Object] The result value to be wrapped.
15
+ def initialize(result)
16
+ super()
17
+ @result = result
18
+ end
19
+
20
+ private
21
+
22
+ # Executes the null response by yielding the result value.
23
+ #
24
+ # @param controller [ActionController::Base] The controller instance (unused).
25
+ # @yield [Object] The result value wrapped by this response.
26
+ # @return [void]
27
+ def execute(controller, &)
28
+ yield @result
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,30 @@
1
+ module Plutonium
2
+ module Interaction
3
+ module Response
4
+ # Represents a redirect response.
5
+ #
6
+ # This class is used to perform redirects as a result of an interaction.
7
+ class Redirect < Base
8
+ # Initializes a new Redirect response.
9
+ #
10
+ # @param path [String, Symbol] The path or named route to redirect to.
11
+ # @param options [Hash] Additional options to pass to the redirect_to method.
12
+ def initialize(path, options = {})
13
+ super()
14
+ @path = path
15
+ @options = options
16
+ end
17
+
18
+ private
19
+
20
+ # Executes the redirect response.
21
+ #
22
+ # @param controller [ActionController::Base] The controller instance.
23
+ # @return [void]
24
+ def execute(controller)
25
+ controller.redirect_to @path, @options
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,28 @@
1
+ module Plutonium
2
+ module Interaction
3
+ module Response
4
+ # Represents a render response.
5
+ #
6
+ # This class is used to render views as a result of an interaction.
7
+ class Render < Base
8
+ # Initializes a new Render response.
9
+ #
10
+ # @param options [Hash] Options to pass to the render method.
11
+ def initialize(options = {})
12
+ super()
13
+ @options = options
14
+ end
15
+
16
+ private
17
+
18
+ # Executes the render response.
19
+ #
20
+ # @param controller [ActionController::Base] The controller instance.
21
+ # @return [void]
22
+ def execute(controller)
23
+ controller.render @options
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,35 +1,96 @@
1
1
  module Plutonium
2
2
  module Lib
3
+ # The BitFlags class provides a convenient way to work with bit flags.
4
+ # It allows setting, checking, and extracting flags using a more
5
+ # readable interface. It supports both symbols and strings as flags.
6
+ #
7
+ # @example Usage
8
+ # flags = BitFlags.new(:read, 'write', :execute)
9
+ # value = flags[:read, 'write'] # => 3
10
+ # flags.set?(value, :read) # => true
11
+ # flags.set?(value, 'execute') # => false
12
+ # flags.extract(value) # => [:read, :write]
13
+ #
3
14
  class BitFlags
4
- delegate :sum, to: :indices
15
+ # @return [Array<Symbol>] An array of all defined flags.
16
+ attr_reader :flags
5
17
 
18
+ # @return [Array<Integer>] An array of all defined bit values.
19
+ attr_reader :indices
20
+
21
+ # Initializes a new BitFlags object with the given flags.
22
+ #
23
+ # @param flags [Array<Symbol, String>] The flags to be used in this BitFlags object.
24
+ # @example
25
+ # BitFlags.new(:read, 'write', :execute)
6
26
  def initialize(*flags)
7
- @map = flags.each_with_index.map { |flag, index| [flag, 2.pow(index)] }.to_h
27
+ @flags = normalize_flags(flags).uniq.freeze
28
+ @indices = @flags.each_index.map { |index| 1 << index }.freeze
29
+ @map = @flags.zip(@indices).to_h.freeze
30
+ @all_bits = calculate_all_bits
8
31
  end
9
32
 
33
+ # Checks if the given value has all the specified flags set.
34
+ #
35
+ # @param value [Integer] The value to check against.
36
+ # @param flags [Array<Symbol, String>] The flags to check for.
37
+ # @return [Boolean] True if all specified flags are set and valid, false otherwise.
38
+ # @example
39
+ # flags.set?(3, :read, 'write') # => true
10
40
  def set?(value, *flags)
11
- check = bits(*flags)
41
+ normalized_flags = normalize_flags(flags)
42
+ return false if normalized_flags.any? { |flag| !@map.key?(flag) }
43
+ check = bits(*normalized_flags)
12
44
  value & check == check
13
45
  end
14
46
 
47
+ # Extracts the flags that are set in the given value.
48
+ #
49
+ # @param value [Integer] The value to extract flags from.
50
+ # @return [Array<Symbol>] An array of flags that are set in the value.
51
+ # @example
52
+ # flags.extract(3) # => [:read, :write]
15
53
  def extract(value)
16
- @map.select { |_flag, bit| value & bit == bit }.keys
54
+ value &= @all_bits
55
+ @map.select { |_, bit| value & bit != 0 }.keys
17
56
  end
18
57
 
58
+ # Returns the bit value for the given flags.
59
+ #
60
+ # @param flags [Array<Symbol, String>] The flags to get the bit value for.
61
+ # @return [Integer] The combined bit value of the given flags.
62
+ # @example
63
+ # flags[:read, 'write'] # => 3
19
64
  def [](*flags)
20
65
  bits(*flags)
21
66
  end
22
67
 
68
+ # Calculates the combined bit value for the given flags.
69
+ #
70
+ # @param flags [Array<Symbol, String>] The flags to calculate the bit value for.
71
+ # @return [Integer] The combined bit value of the given flags.
72
+ # @example
73
+ # flags.bits(:read, 'write') # => 3
23
74
  def bits(*flags)
24
- @map.slice(*flags).values.sum
75
+ normalized_flags = normalize_flags(flags)
76
+ normalized_flags.sum { |flag| @map[flag] || 0 }
77
+ end
78
+
79
+ # Calculates the sum of all bit values.
80
+ #
81
+ # @return [Integer] The sum of all bit values.
82
+ def sum
83
+ @all_bits
25
84
  end
26
85
 
27
- def flags
28
- @flags ||= @map.keys
86
+ private
87
+
88
+ def calculate_all_bits
89
+ @indices.inject(:|) || 0
29
90
  end
30
91
 
31
- def indices
32
- @indices ||= @map.values
92
+ def normalize_flags(flags)
93
+ flags.map(&:to_sym)
33
94
  end
34
95
  end
35
96
  end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ module Lib
5
+ # OverlayedHash provides a hash-like structure that overlays values on top of a base hash.
6
+ #
7
+ # This class allows you to create a new hash-like object that uses a base hash for default values,
8
+ # but can be modified without affecting the original base hash. When a key is accessed, it first
9
+ # checks if the key exists in the overlay; if not, it falls back to the base hash.
10
+ #
11
+ # @example Usage
12
+ # base = { a: 1, b: 2 }
13
+ # overlay = OverlayedHash.new(base)
14
+ # overlay[:b] = 3
15
+ # overlay[:c] = 4
16
+ #
17
+ # overlay[:a] # => 1 (from base)
18
+ # overlay[:b] # => 3 (from overlay)
19
+ # overlay[:c] # => 4 (from overlay)
20
+ # base[:b] # => 2 (unchanged)
21
+ class OverlayedHash
22
+ # Initialize a new OverlayedHash with a base hash.
23
+ #
24
+ # @param base [Hash] The base hash to use for fallback values.
25
+ def initialize(base)
26
+ @base = base
27
+ @overlay = {}
28
+ end
29
+
30
+ # Retrieve a value from the overlay hash or the base hash.
31
+ #
32
+ # @param key The key to look up.
33
+ # @return The value associated with the key, or nil if not found.
34
+ def [](key)
35
+ @overlay.key?(key) ? @overlay[key] : @base[key]
36
+ end
37
+
38
+ # Set a value in the overlay hash.
39
+ #
40
+ # @param key The key to set.
41
+ # @param value The value to associate with the key.
42
+ def []=(key, value)
43
+ @overlay[key] = value
44
+ end
45
+
46
+ # Check if a key exists in either the overlay or base hash.
47
+ #
48
+ # @param key The key to check for.
49
+ # @return [Boolean] true if the key exists, false otherwise.
50
+ def key?(key)
51
+ @overlay.key?(key) || @base.key?(key)
52
+ end
53
+
54
+ # Enumerate over all keys in both the overlay and base hash.
55
+ #
56
+ # @yield [key] Gives each key to the block.
57
+ # @return [Enumerator] If no block is given.
58
+ def each_key
59
+ return to_enum(:each_key) unless block_given?
60
+
61
+ keys.each { |key| yield key }
62
+ end
63
+
64
+ # Retrieve all keys from both the overlay and base hash.
65
+ #
66
+ # @return [Array] An array of all unique keys.
67
+ def keys
68
+ (@overlay.keys + @base.keys).uniq
69
+ end
70
+
71
+ # Retrieve all values, prioritizing the overlay over the base.
72
+ #
73
+ # @return [Array] An array of values corresponding to all keys.
74
+ def values
75
+ keys.map { |key| self[key] }
76
+ end
77
+
78
+ # Convert the OverlayedHash to a regular Hash.
79
+ #
80
+ # @return [Hash] A new Hash with all keys and values from the OverlayedHash.
81
+ def to_h
82
+ keys.each_with_object({}) { |key, hash| hash[key] = self[key] }
83
+ end
84
+ end
85
+ end
86
+ end