claude-agent-framework 1.0.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.
- package/README.md +128 -0
- package/bin/claude-framework +3 -0
- package/framework/agents/design-lead.md +240 -0
- package/framework/agents/product-owner.md +179 -0
- package/framework/agents/tech-lead.md +226 -0
- package/framework/commands/ayuda.md +127 -0
- package/framework/commands/a/303/261adir.md +98 -0
- package/framework/commands/backup.md +397 -0
- package/framework/commands/cambiar.md +110 -0
- package/framework/commands/cloud.md +457 -0
- package/framework/commands/code.md +142 -0
- package/framework/commands/debug.md +334 -0
- package/framework/commands/deploy.md +383 -0
- package/framework/commands/deshacer.md +120 -0
- package/framework/commands/estado.md +218 -0
- package/framework/commands/explica.md +227 -0
- package/framework/commands/feature.md +120 -0
- package/framework/commands/git.md +427 -0
- package/framework/commands/historial.md +202 -0
- package/framework/commands/learn.md +408 -0
- package/framework/commands/movil.md +245 -0
- package/framework/commands/nuevo.md +118 -0
- package/framework/commands/plan.md +134 -0
- package/framework/commands/prd.md +113 -0
- package/framework/commands/probar.md +148 -0
- package/framework/commands/revisar.md +208 -0
- package/framework/commands/seeds.md +230 -0
- package/framework/commands/seguridad.md +226 -0
- package/framework/commands/tasks.md +157 -0
- package/framework/skills/architecture/algorithms.md +970 -0
- package/framework/skills/architecture/clean-code.md +1080 -0
- package/framework/skills/architecture/design-patterns.md +1984 -0
- package/framework/skills/architecture/functional-programming.md +972 -0
- package/framework/skills/architecture/solid.md +991 -0
- package/framework/skills/cloud/cloud-aws.md +848 -0
- package/framework/skills/cloud/cloud-azure.md +931 -0
- package/framework/skills/cloud/cloud-gcp.md +848 -0
- package/framework/skills/cloud/message-queues.md +1229 -0
- package/framework/skills/core/accessibility.md +401 -0
- package/framework/skills/core/api.md +474 -0
- package/framework/skills/core/authentication.md +306 -0
- package/framework/skills/core/authorization.md +388 -0
- package/framework/skills/core/background-jobs.md +341 -0
- package/framework/skills/core/caching.md +473 -0
- package/framework/skills/core/code-review.md +341 -0
- package/framework/skills/core/controllers.md +290 -0
- package/framework/skills/core/cua.md +285 -0
- package/framework/skills/core/documentation.md +472 -0
- package/framework/skills/core/file-uploads.md +351 -0
- package/framework/skills/core/hotwire-native.md +296 -0
- package/framework/skills/core/hotwire.md +278 -0
- package/framework/skills/core/i18n.md +334 -0
- package/framework/skills/core/imports-exports.md +750 -0
- package/framework/skills/core/infrastructure.md +337 -0
- package/framework/skills/core/models.md +228 -0
- package/framework/skills/core/notifications.md +672 -0
- package/framework/skills/core/payments.md +581 -0
- package/framework/skills/core/performance.md +361 -0
- package/framework/skills/core/rails-scaffold.md +131 -0
- package/framework/skills/core/search.md +518 -0
- package/framework/skills/core/security.md +565 -0
- package/framework/skills/core/seeds.md +307 -0
- package/framework/skills/core/seo.md +542 -0
- package/framework/skills/core/testing.md +393 -0
- package/framework/skills/core/views.md +260 -0
- package/framework/skills/core/websockets.md +564 -0
- package/framework/skills/data/advanced-sql.md +1204 -0
- package/framework/skills/data/nosql.md +1141 -0
- package/framework/skills/devops/containers-advanced.md +1237 -0
- package/framework/skills/devops/debugging.md +834 -0
- package/framework/skills/devops/git-workflow.md +752 -0
- package/framework/skills/devops/networking.md +932 -0
- package/framework/skills/devops/shell-scripting.md +1132 -0
- package/framework/sub-agents/architecture-patterns-agent.md +1450 -0
- package/framework/sub-agents/cloud-agent.md +677 -0
- package/framework/sub-agents/data.md +504 -0
- package/framework/sub-agents/debugging-agent.md +554 -0
- package/framework/sub-agents/devops.md +483 -0
- package/framework/sub-agents/docs.md +176 -0
- package/framework/sub-agents/frontend-dev.md +349 -0
- package/framework/sub-agents/git-workflow-agent.md +697 -0
- package/framework/sub-agents/integrations.md +630 -0
- package/framework/sub-agents/native-dev.md +434 -0
- package/framework/sub-agents/qa.md +138 -0
- package/framework/sub-agents/rails-dev.md +375 -0
- package/framework/sub-agents/security.md +526 -0
- package/framework/sub-agents/ui.md +437 -0
- package/framework/sub-agents/ux.md +284 -0
- package/framework/templates/api-spec.md +500 -0
- package/framework/templates/component-spec.md +248 -0
- package/framework/templates/feature.json +13 -0
- package/framework/templates/model-spec.md +318 -0
- package/framework/templates/prd-template.md +80 -0
- package/framework/templates/task-plan.md +122 -0
- package/framework/templates/task-user-story.md +52 -0
- package/framework/templates/technical-spec.md +260 -0
- package/framework/templates/user-story.md +95 -0
- package/package.json +42 -0
- package/project-templates/CLAUDE.md +42 -0
- package/project-templates/contexts/architecture.md +25 -0
- package/project-templates/contexts/conventions.md +46 -0
- package/project-templates/contexts/design-system.md +47 -0
- package/project-templates/contexts/requirements.md +38 -0
- package/project-templates/contexts/stack.md +30 -0
- package/project-templates/history/active/models.md +11 -0
- package/project-templates/history/changelog.md +15 -0
- package/project-templates/workspace/.gitkeep +0 -0
- package/src/cli.js +52 -0
- package/src/init.js +104 -0
- package/src/status.js +75 -0
- package/src/update.js +88 -0
|
@@ -0,0 +1,972 @@
|
|
|
1
|
+
# Skill: Functional Programming in Ruby
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
Apply functional programming concepts in Ruby and Rails to write more predictable, testable, and maintainable code.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Immutability
|
|
9
|
+
|
|
10
|
+
### Freeze Objects to Prevent Mutation
|
|
11
|
+
```ruby
|
|
12
|
+
# Mutable (dangerous)
|
|
13
|
+
CONFIG = { api_key: "secret", timeout: 30 }
|
|
14
|
+
CONFIG[:api_key] = "hacked" # Oops, modified!
|
|
15
|
+
|
|
16
|
+
# Immutable (safe)
|
|
17
|
+
CONFIG = { api_key: "secret", timeout: 30 }.freeze
|
|
18
|
+
CONFIG[:api_key] = "hacked" # => FrozenError
|
|
19
|
+
|
|
20
|
+
# Deep freeze for nested structures
|
|
21
|
+
module DeepFreeze
|
|
22
|
+
def self.freeze(obj)
|
|
23
|
+
case obj
|
|
24
|
+
when Hash
|
|
25
|
+
obj.each_value { |v| freeze(v) }
|
|
26
|
+
when Array
|
|
27
|
+
obj.each { |v| freeze(v) }
|
|
28
|
+
end
|
|
29
|
+
obj.freeze
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
SETTINGS = DeepFreeze.freeze({
|
|
34
|
+
database: { host: "localhost", port: 5432 },
|
|
35
|
+
features: ["auth", "api"]
|
|
36
|
+
})
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Check if Object is Frozen
|
|
40
|
+
```ruby
|
|
41
|
+
string = "hello"
|
|
42
|
+
string.frozen? # => false
|
|
43
|
+
|
|
44
|
+
string.freeze
|
|
45
|
+
string.frozen? # => true
|
|
46
|
+
|
|
47
|
+
# Many Ruby methods return new objects instead of mutating
|
|
48
|
+
original = "hello"
|
|
49
|
+
uppercase = original.upcase # Returns new string
|
|
50
|
+
original # => "hello" (unchanged)
|
|
51
|
+
|
|
52
|
+
# Methods ending with ! typically mutate
|
|
53
|
+
original = "hello"
|
|
54
|
+
original.upcase! # Mutates in place
|
|
55
|
+
original # => "HELLO"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Prefer Non-Mutating Methods
|
|
59
|
+
```ruby
|
|
60
|
+
# Mutating (avoid when possible)
|
|
61
|
+
array = [3, 1, 2]
|
|
62
|
+
array.sort! # Mutates array
|
|
63
|
+
array.reverse! # Mutates array
|
|
64
|
+
|
|
65
|
+
# Non-mutating (preferred)
|
|
66
|
+
array = [3, 1, 2]
|
|
67
|
+
sorted = array.sort # Returns new array
|
|
68
|
+
reversed = array.reverse # Returns new array
|
|
69
|
+
array # => [3, 1, 2] (unchanged)
|
|
70
|
+
|
|
71
|
+
# String methods that don't mutate
|
|
72
|
+
name = "john doe"
|
|
73
|
+
formatted = name.split.map(&:capitalize).join(" ")
|
|
74
|
+
# name is still "john doe"
|
|
75
|
+
|
|
76
|
+
# Rails: dup for ActiveRecord
|
|
77
|
+
user = User.find(1)
|
|
78
|
+
draft = user.dup
|
|
79
|
+
draft.name = "New Name"
|
|
80
|
+
# user.name unchanged
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Immutable Data Structures with Structs
|
|
84
|
+
```ruby
|
|
85
|
+
# Regular struct is mutable
|
|
86
|
+
Point = Struct.new(:x, :y)
|
|
87
|
+
point = Point.new(1, 2)
|
|
88
|
+
point.x = 10 # Mutable
|
|
89
|
+
|
|
90
|
+
# Immutable with Data class (Ruby 3.2+)
|
|
91
|
+
Point = Data.define(:x, :y)
|
|
92
|
+
point = Point.new(x: 1, y: 2)
|
|
93
|
+
point.x = 10 # => NoMethodError
|
|
94
|
+
|
|
95
|
+
# Create modified copy
|
|
96
|
+
new_point = point.with(x: 10) # => Point(x: 10, y: 2)
|
|
97
|
+
point # => Point(x: 1, y: 2) (unchanged)
|
|
98
|
+
|
|
99
|
+
# For older Ruby, use custom immutable struct
|
|
100
|
+
class ImmutablePoint
|
|
101
|
+
attr_reader :x, :y
|
|
102
|
+
|
|
103
|
+
def initialize(x, y)
|
|
104
|
+
@x = x
|
|
105
|
+
@y = y
|
|
106
|
+
freeze
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def with(x: @x, y: @y)
|
|
110
|
+
self.class.new(x, y)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Pure Functions
|
|
118
|
+
|
|
119
|
+
### Definition: Same Input Always Produces Same Output, No Side Effects
|
|
120
|
+
```ruby
|
|
121
|
+
# Impure - depends on external state
|
|
122
|
+
@tax_rate = 0.1
|
|
123
|
+
|
|
124
|
+
def calculate_total(price)
|
|
125
|
+
price * (1 + @tax_rate) # Depends on @tax_rate
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Impure - modifies external state
|
|
129
|
+
def process_order(order)
|
|
130
|
+
@order_count += 1 # Side effect
|
|
131
|
+
order.save # Side effect (database)
|
|
132
|
+
send_email(order) # Side effect (I/O)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Pure - no dependencies, no side effects
|
|
136
|
+
def calculate_total(price, tax_rate)
|
|
137
|
+
price * (1 + tax_rate)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Always returns same result for same input
|
|
141
|
+
calculate_total(100, 0.1) # => 110.0
|
|
142
|
+
calculate_total(100, 0.1) # => 110.0 (always)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Benefits of Pure Functions
|
|
146
|
+
```ruby
|
|
147
|
+
# 1. Easy to test - no setup required
|
|
148
|
+
def add(a, b)
|
|
149
|
+
a + b
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Test
|
|
153
|
+
expect(add(2, 3)).to eq(5) # Simple, no mocks
|
|
154
|
+
|
|
155
|
+
# 2. Easy to reason about
|
|
156
|
+
def full_name(first, last)
|
|
157
|
+
"#{first} #{last}"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# 3. Cacheable (memoization)
|
|
161
|
+
def fibonacci(n)
|
|
162
|
+
return n if n <= 1
|
|
163
|
+
fibonacci(n - 1) + fibonacci(n - 2)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# 4. Parallelizable - no shared state
|
|
167
|
+
results = items.map { |item| pure_transform(item) }
|
|
168
|
+
# Can safely run in parallel
|
|
169
|
+
|
|
170
|
+
# 5. Composable
|
|
171
|
+
def double(x); x * 2; end
|
|
172
|
+
def add_one(x); x + 1; end
|
|
173
|
+
|
|
174
|
+
double(add_one(5)) # => 12
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Pushing Side Effects to the Edges
|
|
178
|
+
```ruby
|
|
179
|
+
# Bad - side effects mixed with logic
|
|
180
|
+
class OrderProcessor
|
|
181
|
+
def process(order)
|
|
182
|
+
return if order.items.empty?
|
|
183
|
+
|
|
184
|
+
total = order.items.sum(&:price)
|
|
185
|
+
tax = total * 0.1
|
|
186
|
+
shipping = total > 100 ? 0 : 10
|
|
187
|
+
final_total = total + tax + shipping
|
|
188
|
+
|
|
189
|
+
order.update!(total: final_total) # Side effect mixed in
|
|
190
|
+
OrderMailer.confirmation(order).deliver_later # Side effect
|
|
191
|
+
InventoryService.reduce_stock(order.items) # Side effect
|
|
192
|
+
|
|
193
|
+
final_total
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Good - pure calculation, side effects at edges
|
|
198
|
+
class OrderCalculator
|
|
199
|
+
# Pure function - no side effects
|
|
200
|
+
def calculate(items, tax_rate: 0.1, free_shipping_threshold: 100)
|
|
201
|
+
return nil if items.empty?
|
|
202
|
+
|
|
203
|
+
subtotal = items.sum(&:price)
|
|
204
|
+
tax = subtotal * tax_rate
|
|
205
|
+
shipping = subtotal > free_shipping_threshold ? 0 : 10
|
|
206
|
+
|
|
207
|
+
{
|
|
208
|
+
subtotal: subtotal,
|
|
209
|
+
tax: tax,
|
|
210
|
+
shipping: shipping,
|
|
211
|
+
total: subtotal + tax + shipping
|
|
212
|
+
}
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
class OrderProcessor
|
|
217
|
+
def initialize(calculator: OrderCalculator.new)
|
|
218
|
+
@calculator = calculator
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def process(order)
|
|
222
|
+
# Pure calculation
|
|
223
|
+
totals = @calculator.calculate(order.items)
|
|
224
|
+
return false unless totals
|
|
225
|
+
|
|
226
|
+
# Side effects at the edge
|
|
227
|
+
persist_order(order, totals)
|
|
228
|
+
send_notifications(order)
|
|
229
|
+
update_inventory(order)
|
|
230
|
+
|
|
231
|
+
true
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
private
|
|
235
|
+
|
|
236
|
+
def persist_order(order, totals)
|
|
237
|
+
order.update!(totals)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def send_notifications(order)
|
|
241
|
+
OrderMailer.confirmation(order).deliver_later
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def update_inventory(order)
|
|
245
|
+
InventoryService.reduce_stock(order.items)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Higher-Order Functions
|
|
253
|
+
|
|
254
|
+
### Functions That Receive Functions
|
|
255
|
+
```ruby
|
|
256
|
+
# map - transform each element
|
|
257
|
+
numbers = [1, 2, 3, 4, 5]
|
|
258
|
+
doubled = numbers.map { |n| n * 2 } # => [2, 4, 6, 8, 10]
|
|
259
|
+
|
|
260
|
+
# select/filter - keep elements matching condition
|
|
261
|
+
evens = numbers.select { |n| n.even? } # => [2, 4]
|
|
262
|
+
|
|
263
|
+
# reject - remove elements matching condition
|
|
264
|
+
odds = numbers.reject { |n| n.even? } # => [1, 3, 5]
|
|
265
|
+
|
|
266
|
+
# reduce/inject - combine elements into single value
|
|
267
|
+
sum = numbers.reduce(0) { |acc, n| acc + n } # => 15
|
|
268
|
+
sum = numbers.reduce(:+) # Shorthand => 15
|
|
269
|
+
|
|
270
|
+
# find/detect - first element matching condition
|
|
271
|
+
first_even = numbers.find { |n| n.even? } # => 2
|
|
272
|
+
|
|
273
|
+
# any? - at least one matches
|
|
274
|
+
numbers.any? { |n| n > 4 } # => true
|
|
275
|
+
|
|
276
|
+
# all? - all match
|
|
277
|
+
numbers.all? { |n| n > 0 } # => true
|
|
278
|
+
|
|
279
|
+
# none? - none match
|
|
280
|
+
numbers.none? { |n| n > 10 } # => true
|
|
281
|
+
|
|
282
|
+
# count - count matching elements
|
|
283
|
+
numbers.count { |n| n.even? } # => 2
|
|
284
|
+
|
|
285
|
+
# partition - split into two arrays
|
|
286
|
+
evens, odds = numbers.partition(&:even?)
|
|
287
|
+
# evens => [2, 4], odds => [1, 3, 5]
|
|
288
|
+
|
|
289
|
+
# group_by - group into hash
|
|
290
|
+
grouped = numbers.group_by { |n| n.even? ? :even : :odd }
|
|
291
|
+
# => { odd: [1, 3, 5], even: [2, 4] }
|
|
292
|
+
|
|
293
|
+
# sort_by - sort by transformation
|
|
294
|
+
users.sort_by { |u| u.created_at }
|
|
295
|
+
users.sort_by(&:created_at) # Shorthand
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Functions That Return Functions
|
|
299
|
+
```ruby
|
|
300
|
+
# Factory for validators
|
|
301
|
+
def minimum_length_validator(min)
|
|
302
|
+
->(string) { string.length >= min }
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
validate_password = minimum_length_validator(8)
|
|
306
|
+
validate_password.call("hello") # => false
|
|
307
|
+
validate_password.call("hello123!") # => true
|
|
308
|
+
|
|
309
|
+
# Factory for comparators
|
|
310
|
+
def compare_by(attribute)
|
|
311
|
+
->(a, b) { a.send(attribute) <=> b.send(attribute) }
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
users.sort(&compare_by(:name))
|
|
315
|
+
users.sort(&compare_by(:created_at))
|
|
316
|
+
|
|
317
|
+
# Decorator/wrapper functions
|
|
318
|
+
def with_logging(func)
|
|
319
|
+
->(*args) do
|
|
320
|
+
puts "Calling with args: #{args}"
|
|
321
|
+
result = func.call(*args)
|
|
322
|
+
puts "Result: #{result}"
|
|
323
|
+
result
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
add = ->(a, b) { a + b }
|
|
328
|
+
logged_add = with_logging(add)
|
|
329
|
+
logged_add.call(2, 3)
|
|
330
|
+
# Output:
|
|
331
|
+
# Calling with args: [2, 3]
|
|
332
|
+
# Result: 5
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## Blocks, Procs, and Lambdas
|
|
338
|
+
|
|
339
|
+
### Blocks
|
|
340
|
+
```ruby
|
|
341
|
+
# Implicit block with yield
|
|
342
|
+
def with_timing
|
|
343
|
+
start = Time.now
|
|
344
|
+
result = yield
|
|
345
|
+
elapsed = Time.now - start
|
|
346
|
+
puts "Took #{elapsed} seconds"
|
|
347
|
+
result
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
with_timing { sleep(1); "done" }
|
|
351
|
+
|
|
352
|
+
# Explicit block with &block
|
|
353
|
+
def transform(items, &block)
|
|
354
|
+
items.map(&block)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
transform([1, 2, 3]) { |n| n * 2 } # => [2, 4, 6]
|
|
358
|
+
|
|
359
|
+
# Check if block given
|
|
360
|
+
def maybe_yield
|
|
361
|
+
if block_given?
|
|
362
|
+
yield
|
|
363
|
+
else
|
|
364
|
+
"No block given"
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Procs
|
|
370
|
+
```ruby
|
|
371
|
+
# Create a Proc
|
|
372
|
+
double = Proc.new { |x| x * 2 }
|
|
373
|
+
double = proc { |x| x * 2 } # Shorthand
|
|
374
|
+
|
|
375
|
+
# Call a Proc
|
|
376
|
+
double.call(5) # => 10
|
|
377
|
+
double.(5) # => 10
|
|
378
|
+
double[5] # => 10
|
|
379
|
+
|
|
380
|
+
# Procs are lenient with arguments
|
|
381
|
+
flexible = proc { |a, b| [a, b] }
|
|
382
|
+
flexible.call(1) # => [1, nil]
|
|
383
|
+
flexible.call(1, 2, 3) # => [1, 2]
|
|
384
|
+
|
|
385
|
+
# Procs return from enclosing method
|
|
386
|
+
def test_proc
|
|
387
|
+
my_proc = proc { return "from proc" }
|
|
388
|
+
my_proc.call
|
|
389
|
+
"after proc" # Never reached
|
|
390
|
+
end
|
|
391
|
+
test_proc # => "from proc"
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Lambdas
|
|
395
|
+
```ruby
|
|
396
|
+
# Create a lambda
|
|
397
|
+
double = lambda { |x| x * 2 }
|
|
398
|
+
double = ->(x) { x * 2 } # Stabby lambda
|
|
399
|
+
|
|
400
|
+
# Call a lambda
|
|
401
|
+
double.call(5) # => 10
|
|
402
|
+
double.(5) # => 10
|
|
403
|
+
double[5] # => 10
|
|
404
|
+
|
|
405
|
+
# Lambdas are strict with arguments
|
|
406
|
+
strict = ->(a, b) { [a, b] }
|
|
407
|
+
strict.call(1) # => ArgumentError
|
|
408
|
+
strict.call(1, 2, 3) # => ArgumentError
|
|
409
|
+
strict.call(1, 2) # => [1, 2]
|
|
410
|
+
|
|
411
|
+
# Lambdas return to caller
|
|
412
|
+
def test_lambda
|
|
413
|
+
my_lambda = -> { return "from lambda" }
|
|
414
|
+
my_lambda.call
|
|
415
|
+
"after lambda" # This IS reached
|
|
416
|
+
end
|
|
417
|
+
test_lambda # => "after lambda"
|
|
418
|
+
|
|
419
|
+
# Lambdas with multiple arguments
|
|
420
|
+
calculate = ->(a, b, operation) { operation.call(a, b) }
|
|
421
|
+
add = ->(x, y) { x + y }
|
|
422
|
+
multiply = ->(x, y) { x * y }
|
|
423
|
+
|
|
424
|
+
calculate.call(2, 3, add) # => 5
|
|
425
|
+
calculate.call(2, 3, multiply) # => 6
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### When to Use Each
|
|
429
|
+
```ruby
|
|
430
|
+
# Use blocks for:
|
|
431
|
+
# - One-time use, inline code
|
|
432
|
+
# - Iterators (each, map, select)
|
|
433
|
+
[1, 2, 3].map { |n| n * 2 }
|
|
434
|
+
|
|
435
|
+
# Use Procs for:
|
|
436
|
+
# - Storing blocks for later
|
|
437
|
+
# - When you need lenient argument handling
|
|
438
|
+
# - When you want return to exit enclosing method
|
|
439
|
+
callback = proc { |result| handle_result(result) }
|
|
440
|
+
|
|
441
|
+
# Use Lambdas for:
|
|
442
|
+
# - Strict argument checking
|
|
443
|
+
# - First-class functions
|
|
444
|
+
# - Functional programming patterns
|
|
445
|
+
validator = ->(value) { value.present? && value.length > 3 }
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Symbol to Proc
|
|
449
|
+
```ruby
|
|
450
|
+
# Long form
|
|
451
|
+
users.map { |user| user.name }
|
|
452
|
+
|
|
453
|
+
# Short form with &
|
|
454
|
+
users.map(&:name)
|
|
455
|
+
|
|
456
|
+
# How it works:
|
|
457
|
+
# &:name calls :name.to_proc
|
|
458
|
+
# :name.to_proc returns a proc that calls .name on its argument
|
|
459
|
+
|
|
460
|
+
# Custom to_proc
|
|
461
|
+
class Integer
|
|
462
|
+
def to_proc
|
|
463
|
+
->(obj) { obj * self }
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
[1, 2, 3].map(&2) # => [2, 4, 6]
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## Method Chaining
|
|
473
|
+
|
|
474
|
+
### Building Fluent Interfaces
|
|
475
|
+
```ruby
|
|
476
|
+
# ActiveRecord is a great example
|
|
477
|
+
User
|
|
478
|
+
.where(active: true)
|
|
479
|
+
.where("created_at > ?", 1.month.ago)
|
|
480
|
+
.includes(:orders)
|
|
481
|
+
.order(created_at: :desc)
|
|
482
|
+
.limit(10)
|
|
483
|
+
|
|
484
|
+
# Custom chainable query builder
|
|
485
|
+
class ProductQuery
|
|
486
|
+
def initialize(scope = Product.all)
|
|
487
|
+
@scope = scope
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def active
|
|
491
|
+
chain { @scope.where(active: true) }
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def in_category(category)
|
|
495
|
+
chain { @scope.where(category: category) }
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def price_between(min, max)
|
|
499
|
+
chain { @scope.where(price: min..max) }
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def search(query)
|
|
503
|
+
return self if query.blank?
|
|
504
|
+
chain { @scope.where("name ILIKE ?", "%#{query}%") }
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def to_a
|
|
508
|
+
@scope.to_a
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def count
|
|
512
|
+
@scope.count
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
private
|
|
516
|
+
|
|
517
|
+
def chain
|
|
518
|
+
self.class.new(yield)
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# Usage
|
|
523
|
+
products = ProductQuery.new
|
|
524
|
+
.active
|
|
525
|
+
.in_category("Electronics")
|
|
526
|
+
.price_between(100, 500)
|
|
527
|
+
.search(params[:q])
|
|
528
|
+
.to_a
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### Tap for Side Effects in Chains
|
|
532
|
+
```ruby
|
|
533
|
+
# tap yields self and returns self
|
|
534
|
+
User.new
|
|
535
|
+
.tap { |u| u.name = "John" }
|
|
536
|
+
.tap { |u| u.email = "john@example.com" }
|
|
537
|
+
.tap { |u| puts "Created user: #{u.name}" }
|
|
538
|
+
.save
|
|
539
|
+
|
|
540
|
+
# Useful for debugging chains
|
|
541
|
+
users
|
|
542
|
+
.select(&:active?)
|
|
543
|
+
.tap { |list| puts "Active users: #{list.count}" }
|
|
544
|
+
.map(&:email)
|
|
545
|
+
.tap { |emails| puts "Emails: #{emails}" }
|
|
546
|
+
.join(", ")
|
|
547
|
+
|
|
548
|
+
# then/yield_self for transformations
|
|
549
|
+
"hello"
|
|
550
|
+
.then { |s| s.upcase }
|
|
551
|
+
.then { |s| "#{s}!" }
|
|
552
|
+
# => "HELLO!"
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
---
|
|
556
|
+
|
|
557
|
+
## Currying and Partial Application
|
|
558
|
+
|
|
559
|
+
### Currying
|
|
560
|
+
Transforms a function that takes multiple arguments into a sequence of functions that each take a single argument.
|
|
561
|
+
|
|
562
|
+
```ruby
|
|
563
|
+
# Regular function
|
|
564
|
+
add = ->(a, b, c) { a + b + c }
|
|
565
|
+
add.call(1, 2, 3) # => 6
|
|
566
|
+
|
|
567
|
+
# Curried function
|
|
568
|
+
curried_add = add.curry
|
|
569
|
+
curried_add.call(1).call(2).call(3) # => 6
|
|
570
|
+
curried_add.(1).(2).(3) # => 6
|
|
571
|
+
|
|
572
|
+
# Partial application with currying
|
|
573
|
+
add_one = curried_add.(1) # Returns a lambda waiting for 2 more args
|
|
574
|
+
add_one_and_two = add_one.(2) # Returns a lambda waiting for 1 more arg
|
|
575
|
+
add_one_and_two.(3) # => 6
|
|
576
|
+
|
|
577
|
+
# Practical example
|
|
578
|
+
multiply = ->(x, y) { x * y }.curry
|
|
579
|
+
|
|
580
|
+
double = multiply.(2)
|
|
581
|
+
triple = multiply.(3)
|
|
582
|
+
|
|
583
|
+
[1, 2, 3, 4].map(&double) # => [2, 4, 6, 8]
|
|
584
|
+
[1, 2, 3, 4].map(&triple) # => [3, 6, 9, 12]
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### Partial Application
|
|
588
|
+
```ruby
|
|
589
|
+
# Using curry for partial application
|
|
590
|
+
greet = ->(greeting, name) { "#{greeting}, #{name}!" }
|
|
591
|
+
say_hello = greet.curry.("Hello")
|
|
592
|
+
say_goodbye = greet.curry.("Goodbye")
|
|
593
|
+
|
|
594
|
+
say_hello.("World") # => "Hello, World!"
|
|
595
|
+
say_goodbye.("World") # => "Goodbye, World!"
|
|
596
|
+
|
|
597
|
+
# Manual partial application
|
|
598
|
+
def partial(func, *fixed_args)
|
|
599
|
+
->(*args) { func.call(*fixed_args, *args) }
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
log = ->(level, message) { puts "[#{level}] #{message}" }
|
|
603
|
+
info = partial(log, "INFO")
|
|
604
|
+
error = partial(log, "ERROR")
|
|
605
|
+
|
|
606
|
+
info.("Application started") # => [INFO] Application started
|
|
607
|
+
error.("Something went wrong") # => [ERROR] Something went wrong
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
## Function Composition
|
|
613
|
+
|
|
614
|
+
### Composing Functions
|
|
615
|
+
```ruby
|
|
616
|
+
# Manual composition
|
|
617
|
+
double = ->(x) { x * 2 }
|
|
618
|
+
add_one = ->(x) { x + 1 }
|
|
619
|
+
|
|
620
|
+
# Compose: add_one after double
|
|
621
|
+
composed = ->(x) { add_one.(double.(x)) }
|
|
622
|
+
composed.(5) # => 11 (5 * 2 + 1)
|
|
623
|
+
|
|
624
|
+
# Compose helper
|
|
625
|
+
def compose(*functions)
|
|
626
|
+
->(arg) { functions.reverse.reduce(arg) { |result, fn| fn.(result) } }
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
pipeline = compose(add_one, double, add_one)
|
|
630
|
+
pipeline.(5) # => 13 ((5 + 1) * 2 + 1)
|
|
631
|
+
|
|
632
|
+
# Using >> and << operators (Ruby 2.6+)
|
|
633
|
+
double = ->(x) { x * 2 }
|
|
634
|
+
add_one = ->(x) { x + 1 }
|
|
635
|
+
|
|
636
|
+
# >> composes left to right
|
|
637
|
+
(double >> add_one).call(5) # => 11 (5 * 2, then + 1)
|
|
638
|
+
|
|
639
|
+
# << composes right to left
|
|
640
|
+
(double << add_one).call(5) # => 12 (5 + 1, then * 2)
|
|
641
|
+
|
|
642
|
+
# Chain multiple
|
|
643
|
+
square = ->(x) { x ** 2 }
|
|
644
|
+
pipeline = double >> add_one >> square
|
|
645
|
+
pipeline.(3) # => 49 ((3 * 2 + 1) ** 2)
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### Practical Composition in Rails
|
|
649
|
+
```ruby
|
|
650
|
+
# Compose transformations
|
|
651
|
+
class DataPipeline
|
|
652
|
+
def initialize
|
|
653
|
+
@steps = []
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
def add_step(step)
|
|
657
|
+
@steps << step
|
|
658
|
+
self
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
def process(data)
|
|
662
|
+
@steps.reduce(data) { |result, step| step.call(result) }
|
|
663
|
+
end
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
# Define transformation steps
|
|
667
|
+
strip_whitespace = ->(data) { data.transform_values(&:strip) }
|
|
668
|
+
downcase_email = ->(data) { data.merge(email: data[:email]&.downcase) }
|
|
669
|
+
validate = ->(data) { data[:email]&.include?("@") ? data : raise("Invalid email") }
|
|
670
|
+
normalize_phone = ->(data) { data.merge(phone: data[:phone]&.gsub(/\D/, "")) }
|
|
671
|
+
|
|
672
|
+
# Build pipeline
|
|
673
|
+
pipeline = DataPipeline.new
|
|
674
|
+
.add_step(strip_whitespace)
|
|
675
|
+
.add_step(downcase_email)
|
|
676
|
+
.add_step(normalize_phone)
|
|
677
|
+
.add_step(validate)
|
|
678
|
+
|
|
679
|
+
# Use
|
|
680
|
+
clean_data = pipeline.process({
|
|
681
|
+
name: " John ",
|
|
682
|
+
email: " JOHN@EXAMPLE.COM ",
|
|
683
|
+
phone: "(555) 123-4567"
|
|
684
|
+
})
|
|
685
|
+
# => { name: "John", email: "john@example.com", phone: "5551234567" }
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
---
|
|
689
|
+
|
|
690
|
+
## Lazy Evaluation
|
|
691
|
+
|
|
692
|
+
### Enumerator::Lazy
|
|
693
|
+
```ruby
|
|
694
|
+
# Eager - processes all elements immediately
|
|
695
|
+
(1..Float::INFINITY)
|
|
696
|
+
.select { |n| n.even? }
|
|
697
|
+
.first(5)
|
|
698
|
+
# => Never finishes! (infinite sequence)
|
|
699
|
+
|
|
700
|
+
# Lazy - processes elements on demand
|
|
701
|
+
(1..Float::INFINITY)
|
|
702
|
+
.lazy
|
|
703
|
+
.select { |n| n.even? }
|
|
704
|
+
.first(5)
|
|
705
|
+
# => [2, 4, 6, 8, 10]
|
|
706
|
+
|
|
707
|
+
# Useful for large files
|
|
708
|
+
File.open("huge_file.csv")
|
|
709
|
+
.lazy
|
|
710
|
+
.map { |line| line.split(",") }
|
|
711
|
+
.select { |row| row[0] == "active" }
|
|
712
|
+
.first(100)
|
|
713
|
+
# Only reads enough lines to get 100 matches
|
|
714
|
+
|
|
715
|
+
# Chaining lazy operations
|
|
716
|
+
numbers = (1..1_000_000).lazy
|
|
717
|
+
|
|
718
|
+
result = numbers
|
|
719
|
+
.select { |n| n % 3 == 0 } # Divisible by 3
|
|
720
|
+
.map { |n| n * 2 } # Double it
|
|
721
|
+
.select { |n| n.to_s.include?("7") } # Contains 7
|
|
722
|
+
.first(10)
|
|
723
|
+
# Only processes what's needed to find 10 matches
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
### Custom Lazy Enumerators
|
|
727
|
+
```ruby
|
|
728
|
+
# Lazy API paginator
|
|
729
|
+
class LazyApiPaginator
|
|
730
|
+
include Enumerable
|
|
731
|
+
|
|
732
|
+
def initialize(client, resource)
|
|
733
|
+
@client = client
|
|
734
|
+
@resource = resource
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
def each
|
|
738
|
+
return to_enum(:each).lazy unless block_given?
|
|
739
|
+
|
|
740
|
+
page = 1
|
|
741
|
+
loop do
|
|
742
|
+
response = @client.get(@resource, page: page)
|
|
743
|
+
break if response.data.empty?
|
|
744
|
+
|
|
745
|
+
response.data.each { |item| yield item }
|
|
746
|
+
page += 1
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
# Usage - only fetches pages as needed
|
|
752
|
+
paginator = LazyApiPaginator.new(api_client, "/users")
|
|
753
|
+
paginator.lazy.select { |u| u["active"] }.first(10)
|
|
754
|
+
# Stops fetching pages once we have 10 active users
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
### Memoization as Lazy Evaluation
|
|
758
|
+
```ruby
|
|
759
|
+
# Using ||= for lazy initialization
|
|
760
|
+
class ExpensiveCalculator
|
|
761
|
+
def result
|
|
762
|
+
@result ||= calculate_expensive_value
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
private
|
|
766
|
+
|
|
767
|
+
def calculate_expensive_value
|
|
768
|
+
puts "Calculating..."
|
|
769
|
+
sleep(2)
|
|
770
|
+
42
|
|
771
|
+
end
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
calc = ExpensiveCalculator.new
|
|
775
|
+
calc.result # Calculates (takes 2 seconds)
|
|
776
|
+
calc.result # Returns cached value (instant)
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
---
|
|
780
|
+
|
|
781
|
+
## Functional Patterns in Rails
|
|
782
|
+
|
|
783
|
+
### Service Objects as Pure Functions
|
|
784
|
+
```ruby
|
|
785
|
+
class CalculateShipping
|
|
786
|
+
def self.call(weight:, destination:, expedited: false)
|
|
787
|
+
new(weight, destination, expedited).call
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
def initialize(weight, destination, expedited)
|
|
791
|
+
@weight = weight
|
|
792
|
+
@destination = destination
|
|
793
|
+
@expedited = expedited
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
def call
|
|
797
|
+
base_rate = calculate_base_rate
|
|
798
|
+
distance_factor = calculate_distance_factor
|
|
799
|
+
expedited_factor = @expedited ? 1.5 : 1.0
|
|
800
|
+
|
|
801
|
+
(base_rate * distance_factor * expedited_factor).round(2)
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
private
|
|
805
|
+
|
|
806
|
+
def calculate_base_rate
|
|
807
|
+
case @weight
|
|
808
|
+
when 0..1 then 5.0
|
|
809
|
+
when 1..5 then 10.0
|
|
810
|
+
when 5..20 then 20.0
|
|
811
|
+
else 30.0 + (@weight - 20) * 1.5
|
|
812
|
+
end
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
def calculate_distance_factor
|
|
816
|
+
DISTANCE_FACTORS.fetch(@destination, 1.0)
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
DISTANCE_FACTORS = {
|
|
820
|
+
local: 1.0,
|
|
821
|
+
regional: 1.25,
|
|
822
|
+
national: 1.5,
|
|
823
|
+
international: 2.5
|
|
824
|
+
}.freeze
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
# Pure - same inputs always produce same output
|
|
828
|
+
CalculateShipping.call(weight: 3, destination: :regional) # => 12.5
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
### Transformation Pipelines
|
|
832
|
+
```ruby
|
|
833
|
+
class ImportUsersPipeline
|
|
834
|
+
TRANSFORMATIONS = [
|
|
835
|
+
:parse_csv,
|
|
836
|
+
:normalize_data,
|
|
837
|
+
:validate_records,
|
|
838
|
+
:deduplicate,
|
|
839
|
+
:transform_to_users
|
|
840
|
+
].freeze
|
|
841
|
+
|
|
842
|
+
def call(file_content)
|
|
843
|
+
TRANSFORMATIONS.reduce(file_content) do |data, transformation|
|
|
844
|
+
send(transformation, data)
|
|
845
|
+
end
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
private
|
|
849
|
+
|
|
850
|
+
def parse_csv(content)
|
|
851
|
+
CSV.parse(content, headers: true).map(&:to_h)
|
|
852
|
+
end
|
|
853
|
+
|
|
854
|
+
def normalize_data(records)
|
|
855
|
+
records.map do |record|
|
|
856
|
+
record.transform_keys(&:downcase)
|
|
857
|
+
.transform_values(&:strip)
|
|
858
|
+
end
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
def validate_records(records)
|
|
862
|
+
records.select { |r| r["email"].present? }
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
def deduplicate(records)
|
|
866
|
+
records.uniq { |r| r["email"].downcase }
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
def transform_to_users(records)
|
|
870
|
+
records.map { |r| UserBuilder.from_hash(r) }
|
|
871
|
+
end
|
|
872
|
+
end
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
### Functional Error Handling with Result Objects
|
|
876
|
+
```ruby
|
|
877
|
+
class Result
|
|
878
|
+
attr_reader :value, :error
|
|
879
|
+
|
|
880
|
+
def self.success(value)
|
|
881
|
+
new(value: value, success: true)
|
|
882
|
+
end
|
|
883
|
+
|
|
884
|
+
def self.failure(error)
|
|
885
|
+
new(error: error, success: false)
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
def initialize(value: nil, error: nil, success:)
|
|
889
|
+
@value = value
|
|
890
|
+
@error = error
|
|
891
|
+
@success = success
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
def success?
|
|
895
|
+
@success
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
def failure?
|
|
899
|
+
!@success
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
# Monadic bind/flatmap
|
|
903
|
+
def and_then(&block)
|
|
904
|
+
return self if failure?
|
|
905
|
+
block.call(@value)
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
# Functor map
|
|
909
|
+
def map(&block)
|
|
910
|
+
return self if failure?
|
|
911
|
+
Result.success(block.call(@value))
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
# Handle error
|
|
915
|
+
def or_else(&block)
|
|
916
|
+
return self if success?
|
|
917
|
+
block.call(@error)
|
|
918
|
+
end
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
# Usage with chaining
|
|
922
|
+
class RegisterUser
|
|
923
|
+
def call(params)
|
|
924
|
+
validate(params)
|
|
925
|
+
.and_then { |p| check_uniqueness(p) }
|
|
926
|
+
.and_then { |p| create_user(p) }
|
|
927
|
+
.and_then { |user| send_welcome_email(user) }
|
|
928
|
+
end
|
|
929
|
+
|
|
930
|
+
private
|
|
931
|
+
|
|
932
|
+
def validate(params)
|
|
933
|
+
if params[:email].present? && params[:password].length >= 8
|
|
934
|
+
Result.success(params)
|
|
935
|
+
else
|
|
936
|
+
Result.failure("Invalid params")
|
|
937
|
+
end
|
|
938
|
+
end
|
|
939
|
+
|
|
940
|
+
def check_uniqueness(params)
|
|
941
|
+
if User.exists?(email: params[:email])
|
|
942
|
+
Result.failure("Email already taken")
|
|
943
|
+
else
|
|
944
|
+
Result.success(params)
|
|
945
|
+
end
|
|
946
|
+
end
|
|
947
|
+
|
|
948
|
+
def create_user(params)
|
|
949
|
+
user = User.create(params)
|
|
950
|
+
if user.persisted?
|
|
951
|
+
Result.success(user)
|
|
952
|
+
else
|
|
953
|
+
Result.failure(user.errors.full_messages.join(", "))
|
|
954
|
+
end
|
|
955
|
+
end
|
|
956
|
+
|
|
957
|
+
def send_welcome_email(user)
|
|
958
|
+
UserMailer.welcome(user).deliver_later
|
|
959
|
+
Result.success(user)
|
|
960
|
+
end
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
# In controller
|
|
964
|
+
result = RegisterUser.new.call(user_params)
|
|
965
|
+
|
|
966
|
+
if result.success?
|
|
967
|
+
redirect_to dashboard_path, notice: "Welcome!"
|
|
968
|
+
else
|
|
969
|
+
flash.now[:alert] = result.error
|
|
970
|
+
render :new
|
|
971
|
+
end
|
|
972
|
+
```
|