rails-hyperdrive-layered-rails 0.1.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.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +30 -0
  3. data/README.md +89 -0
  4. data/UPSTREAM +4 -0
  5. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/SKILL.md.erb +315 -0
  6. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/authorization-to-policy.md +79 -0
  7. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/callbacks-to-service.md +72 -0
  8. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/complex-input-to-form-object.md +119 -0
  9. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/current-from-model.md +63 -0
  10. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/god-object-decomposition.md +81 -0
  11. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/implicit-to-explicit-state-machine.md +77 -0
  12. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/query-to-query-object.md +63 -0
  13. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/view-logic-to-presenter.md +60 -0
  14. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/callbacks.md +114 -0
  15. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/concerns.md +92 -0
  16. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/helpers.md +66 -0
  17. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/jobs.md +71 -0
  18. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/layer-violations.md +176 -0
  19. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/service-objects.md +131 -0
  20. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/testing.md +37 -0
  21. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/core/architecture-layers.md +204 -0
  22. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/core/extraction-signals.md +314 -0
  23. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/core/specification-test.md +229 -0
  24. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/action-policy.md +437 -0
  25. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/active-agent.md +275 -0
  26. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/active-delivery.md +221 -0
  27. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/active-job-performs.md +171 -0
  28. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/alba.md +257 -0
  29. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/anyway-config.md +212 -0
  30. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/rubanok.md +246 -0
  31. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/view-component.md +223 -0
  32. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/workflow.md +305 -0
  33. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/collaborator-objects.md +209 -0
  34. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/concerns.md +381 -0
  35. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/filter-objects.md +222 -0
  36. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/form-objects.md +267 -0
  37. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/policy-objects.md +298 -0
  38. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/presenters.md +257 -0
  39. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/query-objects.md +208 -0
  40. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/repositories.md +382 -0
  41. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/serializers.md +256 -0
  42. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/service-objects.md +170 -0
  43. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/state-machines.md +368 -0
  44. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/value-objects.md +267 -0
  45. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/ai-integration.md +372 -0
  46. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/authorization.md +398 -0
  47. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/callbacks.md +336 -0
  48. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/configuration.md +383 -0
  49. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/current-attributes.md +307 -0
  50. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/instrumentation.md +386 -0
  51. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/notifications.md +373 -0
  52. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/view-components.md +454 -0
  53. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/analyze-callbacks.md +255 -0
  54. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/analyze-gods.md +279 -0
  55. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/analyze-services.md +1444 -0
  56. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/analyze.md +376 -0
  57. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/plan.md +249 -0
  58. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/review.md +473 -0
  59. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/spec-test.md +282 -0
  60. data/lib/rails-hyperdrive-layered-rails/version.rb +3 -0
  61. data/lib/rails-hyperdrive-layered-rails.rb +7 -0
  62. metadata +106 -0
@@ -0,0 +1,305 @@
1
+ # Workflow
2
+
3
+ Finite state machine implementation for Ruby.
4
+
5
+ **GitHub**: https://github.com/geekq/workflow
6
+ **Layer**: Domain
7
+
8
+ ## Contents
9
+
10
+ - Installation
11
+ - Basic Usage
12
+ - Per-Event Callback Methods
13
+ - Transition Guards
14
+ - State-Dependent Behavior
15
+ - Standalone Workflow
16
+ - Generic `on_transition` Hook
17
+ - Querying by State
18
+ - Testing
19
+
20
+ ## Installation
21
+
22
+ ```ruby
23
+ # Gemfile
24
+ gem "workflow"
25
+ gem "workflow-activerecord" # For Active Record integration
26
+ ```
27
+
28
+ ## Basic Usage
29
+
30
+ ### Define State Machine
31
+
32
+ ```ruby
33
+ class Post < ApplicationRecord
34
+ include WorkflowActiverecord
35
+
36
+ workflow_column :status
37
+
38
+ workflow do
39
+ state :draft do
40
+ event :submit, transitions_to: :pending_review
41
+ end
42
+
43
+ state :pending_review do
44
+ event :approve, transitions_to: :approved
45
+ event :reject, transitions_to: :rejected
46
+ event :request_changes, transitions_to: :draft
47
+ end
48
+
49
+ state :approved do
50
+ event :publish, transitions_to: :published
51
+ event :archive, transitions_to: :archived
52
+ end
53
+
54
+ state :rejected do
55
+ event :revise, transitions_to: :draft
56
+ end
57
+
58
+ state :published do
59
+ event :archive, transitions_to: :archived
60
+ event :unpublish, transitions_to: :draft
61
+ end
62
+
63
+ state :archived do
64
+ event :restore, transitions_to: :draft
65
+ end
66
+ end
67
+ end
68
+ ```
69
+
70
+ ### Trigger Transitions
71
+
72
+ ```ruby
73
+ post = Post.create!(title: "My Post")
74
+ post.draft? #=> true
75
+
76
+ post.submit!
77
+ post.pending_review? #=> true
78
+
79
+ post.approve!
80
+ post.approved? #=> true
81
+
82
+ post.publish!
83
+ post.published? #=> true
84
+ ```
85
+
86
+ ### Check Available Events
87
+
88
+ ```ruby
89
+ post.current_state.events
90
+ #=> [:submit]
91
+
92
+ post.can_submit? #=> true
93
+ post.can_approve? #=> false
94
+ ```
95
+
96
+ ## Per-Event Callback Methods
97
+
98
+ ```ruby
99
+ class Post < ApplicationRecord
100
+ include WorkflowActiverecord
101
+
102
+ workflow do
103
+ state :draft do
104
+ event :submit, transitions_to: :pending_review
105
+ end
106
+
107
+ state :pending_review do
108
+ event :approve, transitions_to: :approved
109
+ end
110
+
111
+ state :approved do
112
+ event :publish, transitions_to: :published
113
+ end
114
+ end
115
+
116
+ # Called when event fires
117
+ def submit
118
+ self.submitted_at = Time.current
119
+ end
120
+
121
+ def approve
122
+ self.approved_at = Time.current
123
+ self.approved_by = Current.user
124
+ end
125
+
126
+ def publish
127
+ self.published_at = Time.current
128
+ end
129
+ end
130
+ ```
131
+
132
+ ## Transition Guards
133
+
134
+ ```ruby
135
+ class Post < ApplicationRecord
136
+ include WorkflowActiverecord
137
+
138
+ workflow do
139
+ state :draft do
140
+ event :submit, transitions_to: :pending_review,
141
+ if: :submittable?
142
+ end
143
+
144
+ state :pending_review do
145
+ event :approve, transitions_to: :approved,
146
+ if: proc { Current.user&.admin? }
147
+ end
148
+ end
149
+
150
+ private
151
+
152
+ def submittable?
153
+ title.present? && body.present?
154
+ end
155
+ end
156
+ ```
157
+
158
+ ## State-Dependent Behavior
159
+
160
+ ```ruby
161
+ class Post < ApplicationRecord
162
+ include WorkflowActiverecord
163
+
164
+ workflow do
165
+ state :draft
166
+ state :published
167
+ state :archived
168
+ end
169
+
170
+ def editable?
171
+ draft?
172
+ end
173
+
174
+ def visible?
175
+ published?
176
+ end
177
+
178
+ def restorable?
179
+ archived?
180
+ end
181
+ end
182
+ ```
183
+
184
+ ## Standalone Workflow
185
+
186
+ When state machine isn't central to the model:
187
+
188
+ ```ruby
189
+ class Post::PublicationWorkflow
190
+ include Workflow
191
+
192
+ private attr_reader :post
193
+
194
+ def initialize(post)
195
+ @post = post
196
+ @state = post.publication_status
197
+ end
198
+
199
+ workflow do
200
+ state :draft do
201
+ event :submit, transitions_to: :submitted
202
+ end
203
+
204
+ state :submitted do
205
+ event :approve, transitions_to: :approved
206
+ event :reject, transitions_to: :rejected
207
+ end
208
+
209
+ state :approved do
210
+ event :publish, transitions_to: :published
211
+ end
212
+
213
+ state :published do
214
+ event :archive, transitions_to: :archived
215
+ end
216
+ end
217
+
218
+ def persist_workflow_state(new_state)
219
+ post.update!(publication_status: new_state)
220
+ end
221
+
222
+ def publish
223
+ post.touch(:published_at)
224
+ end
225
+ end
226
+
227
+ # Usage
228
+ class Post < ApplicationRecord
229
+ def publication_workflow
230
+ @publication_workflow ||= PublicationWorkflow.new(self)
231
+ end
232
+
233
+ delegate :submit!, :approve!, :publish!, to: :publication_workflow
234
+ end
235
+ ```
236
+
237
+ ## Generic `on_transition` Hook
238
+
239
+ ```ruby
240
+ class Order < ApplicationRecord
241
+ include WorkflowActiverecord
242
+
243
+ workflow do
244
+ state :pending do
245
+ event :confirm, transitions_to: :confirmed
246
+ end
247
+
248
+ state :confirmed do
249
+ event :ship, transitions_to: :shipped
250
+ end
251
+
252
+ state :shipped do
253
+ event :deliver, transitions_to: :delivered
254
+ end
255
+ end
256
+
257
+ # Runs after any transition
258
+ def on_transition(from, to, event, *args)
259
+ AuditLog.create!(
260
+ record: self,
261
+ from_state: from,
262
+ to_state: to,
263
+ event: event
264
+ )
265
+ end
266
+ end
267
+ ```
268
+
269
+ ## Querying by State
270
+
271
+ ```ruby
272
+ # Workflow adds scopes for each state
273
+ Post.with_draft_status
274
+ Post.with_published_status
275
+
276
+ # Or use enum-style queries if using enum column
277
+ Post.where(status: :draft)
278
+ ```
279
+
280
+ ## Testing
281
+
282
+ ```ruby
283
+ RSpec.describe Post do
284
+ describe "workflow" do
285
+ it "starts in draft state" do
286
+ post = Post.new
287
+ expect(post).to be_draft
288
+ end
289
+
290
+ it "transitions from draft to pending_review" do
291
+ post = create(:post, :draft)
292
+
293
+ expect { post.submit! }
294
+ .to change { post.status }
295
+ .from("draft").to("pending_review")
296
+ end
297
+
298
+ it "guards against invalid submissions" do
299
+ post = create(:post, title: nil)
300
+
301
+ expect { post.submit! }.to raise_error(Workflow::NoTransitionAllowed)
302
+ end
303
+ end
304
+ end
305
+ ```
@@ -0,0 +1,209 @@
1
+ # Collaborator Objects
2
+
3
+ ## Contents
4
+
5
+ - Summary
6
+ - When to Use
7
+ - When NOT to Use
8
+ - Key Principles
9
+ - Implementation
10
+ - Comparison With Other Patterns
11
+ - Anti-Patterns
12
+
13
+ ## Summary
14
+
15
+ A collaborator object is a small, focused class that owns a slice of behavior tightly coupled to a primary model. It lives alongside the model, takes the model (or a piece of its data) as input, and exposes a narrow interface. The classic shape is a **delegate object**: a separate class — sometimes a separate Active Record table — that the primary model delegates a coherent group of attributes and methods to.
16
+
17
+ A collaborator is **not** a service object. Services orchestrate operations across the application. Collaborators stay inside the Domain layer; they exist to keep the primary model from accumulating unrelated responsibilities.
18
+
19
+ ## When to Use
20
+
21
+ - A model has a coherent slice of behavior that doesn't justify a full sibling model but bloats the primary class (contact information, profile metadata, billing details).
22
+ - Multiple models share the slice (polymorphic delegates work well here).
23
+ - The slice has its own validations, normalizations, or formatting rules that don't belong on the parent.
24
+ - A behavioral concern has grown beyond a single mixin's reasonable size and would be clearer as a typed object.
25
+
26
+ ## When NOT to Use
27
+
28
+ - The slice is just data with no behavior — use a value object instead.
29
+ - The slice is a unit of work (creating, sending, syncing) — use a service object.
30
+ - The slice is reusable across heterogeneous models with no per-instance state — a concern is fine.
31
+ - The slice has its own identity and lifecycle — promote it to a sibling model.
32
+
33
+ ## Key Principles
34
+
35
+ - **One primary association.** A collaborator is created for and owned by a single model (or polymorphic owner). It does not stand alone.
36
+ - **Narrow interface.** The collaborator exposes a focused set of methods — what its slice of behavior provides — not the full Active Record API.
37
+ - **Domain-layer only.** A collaborator stays inside the Domain layer. It does not call mailers, jobs, services, or external APIs. If it needs to, it's a service in disguise.
38
+ - **Delegate from the parent.** The parent model uses `delegate :method, to: :collaborator` (or a concern that does the same) so callers can keep using the parent's existing API while the implementation moves.
39
+
40
+ ## Implementation
41
+
42
+ ### Polymorphic Delegate (Active Record)
43
+
44
+ The contact-information example: a `User` had grown phone-number, country-code, social-account, and visibility validations and normalizations that all clustered around contacting a person. Extracting them into `ContactInformation` keeps the user model focused on identity and authentication.
45
+
46
+ ```ruby
47
+ class ContactInformation < ApplicationRecord
48
+ belongs_to :contactable, polymorphic: true
49
+
50
+ SOCIAL_ACCOUNTS = %i[facebook twitter tiktok].freeze
51
+ store_accessor :social_accounts, *SOCIAL_ACCOUNTS, suffix: :social_id
52
+
53
+ validates :phone_number, allow_blank: true, phone: {types: :mobile}
54
+ validates :country_code, inclusion: Country.codes
55
+
56
+ normalizes :phone_number, with: -> { Phonelib.parse(it).e164 }
57
+
58
+ def region
59
+ Country[country_code].region
60
+ end
61
+
62
+ def phone_number_visible?
63
+ contact_info_visible && phone_number_visible
64
+ end
65
+ end
66
+
67
+ module Contactable
68
+ extend ActiveSupport::Concern
69
+
70
+ included do
71
+ has_one :contact_information, as: :contactable, dependent: :destroy
72
+
73
+ delegate :phone_number, :region, to: :contact_information
74
+ end
75
+ end
76
+
77
+ class User < ApplicationRecord
78
+ include Contactable
79
+ end
80
+ ```
81
+
82
+ Callers keep using `user.phone_number` and `user.region` — the delegate is invisible at the call site.
83
+
84
+ ### Plain Ruby Delegate
85
+
86
+ A collaborator does not have to be an Active Record model. When the slice does not need persistence, a plain Ruby object that takes the parent in its constructor is enough.
87
+
88
+ ```ruby
89
+ class User
90
+ class Greeter
91
+ def initialize(user)
92
+ @user = user
93
+ end
94
+
95
+ def hello
96
+ "Hello, #{@user.name}!"
97
+ end
98
+
99
+ def goodbye
100
+ "See you later, #{@user.name}."
101
+ end
102
+ end
103
+
104
+ def greeter
105
+ @greeter ||= Greeter.new(self)
106
+ end
107
+
108
+ delegate :hello, :goodbye, to: :greeter
109
+ end
110
+
111
+ user.hello #=> "Hello, Alice!"
112
+ user.goodbye #=> "See you later, Alice."
113
+ ```
114
+
115
+ ### Delegate Replacing a Mixed Concern
116
+
117
+ When a concern accumulates unrelated behavior — validation rules, query helpers, formatting — splitting it into a typed delegate (or several) makes ownership clear.
118
+
119
+ ```ruby
120
+ # Before: User concern is a junk drawer
121
+ module Contactable
122
+ extend ActiveSupport::Concern
123
+
124
+ included do
125
+ validates :phone_number, ...
126
+ normalizes :phone_number, ...
127
+ end
128
+
129
+ def region = ...
130
+ def phone_number_visible? = ...
131
+ def social_links = ...
132
+ def primary_channel = ...
133
+ end
134
+
135
+ # After: contact information owns its own type
136
+ class ContactInformation < ApplicationRecord
137
+ validates :phone_number, ...
138
+ normalizes :phone_number, ...
139
+
140
+ def region = ...
141
+ def phone_number_visible? = ...
142
+ def social_links = ...
143
+ def primary_channel = ...
144
+ end
145
+
146
+ module Contactable
147
+ extend ActiveSupport::Concern
148
+
149
+ included do
150
+ has_one :contact_information, as: :contactable
151
+ delegate :region, :phone_number_visible?, :social_links, :primary_channel,
152
+ to: :contact_information
153
+ end
154
+ end
155
+ ```
156
+
157
+ The `Contactable` concern shrinks to a one-liner association + delegate. The behavior moves to a class that names what it represents.
158
+
159
+ ## Comparison With Other Patterns
160
+
161
+ | Pattern | Owns | Has behavior? | Has identity? | Layer |
162
+ |---|---|---|---|---|
163
+ | **Value object** | A bundle of attributes | Yes (pure) | No | Domain |
164
+ | **Collaborator / delegate object** | A slice of one model's behavior | Yes (and possibly state) | Optional (polymorphic AR has its own row) | Domain |
165
+ | **Service object** | A unit of work | Yes (orchestration) | No (transient) | Application |
166
+ | **Sibling model** | A first-class concept | Yes | Yes (own primary key) | Domain |
167
+ | **Concern** | A reusable behavior across models | Yes (mixed in) | No | Domain |
168
+
169
+ The collaborator/delegate sits between value objects and sibling models: it has more behavior than a value object and less independent identity than a sibling model. It is the right tool when a slice of a model wants its own type but does not want to be its own entity.
170
+
171
+ ## Anti-Patterns
172
+
173
+ ### Cross-Layer Delegate
174
+
175
+ ```ruby
176
+ # BAD — crosses into Infrastructure
177
+ class User::Notifier
178
+ def initialize(user) = @user = user
179
+
180
+ def notify(event)
181
+ NotifierMailer.send(@user, event).deliver_later
182
+ SlackClient.post(@user.slack_id, event)
183
+ end
184
+ end
185
+
186
+ class User < ApplicationRecord
187
+ delegate :notify, to: :notifier
188
+ def notifier = @notifier ||= Notifier.new(self)
189
+ end
190
+ ```
191
+
192
+ Calling mailers, jobs, or external APIs from a delegate makes it an application service in domain clothing. Move it to `app/services/` or to the dedicated notification layer.
193
+
194
+ ### Anemic Delegate
195
+
196
+ ```ruby
197
+ # BAD — just a struct of attribute readers
198
+ class User::Profile
199
+ def initialize(user) = @user = user
200
+ def first_name = @user.first_name
201
+ def last_name = @user.last_name
202
+ end
203
+ ```
204
+
205
+ If the delegate has no behavior of its own, it is paperwork. Either inline the methods on the parent, or promote to a value object if the attributes deserve their own type.
206
+
207
+ ### Delegate That Knows About Callers
208
+
209
+ A collaborator should not branch on who called it (`if controller_context.user.admin?`). It owns a slice of one model's behavior; cross-cutting context belongs in the calling layer.