use_case 0.6.0 → 0.7.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.
- data/Readme.md +80 -32
- data/lib/use_case/version.rb +1 -1
- data/lib/use_case.rb +18 -13
- data/test/sample_use_case.rb +39 -14
- data/test/use_case_test.rb +14 -0
- metadata +2 -2
data/Readme.md
CHANGED
@@ -155,11 +155,12 @@ class CreateRepository
|
|
155
155
|
# any dependencies you need.
|
156
156
|
def initialize(auth, user)
|
157
157
|
input_class(NewRepositoryInput)
|
158
|
-
|
159
|
-
|
160
|
-
# A command
|
161
|
-
#
|
162
|
-
|
158
|
+
add_pre_condition(UserLoggedInPrecondition.new(user))
|
159
|
+
add_pre_condition(ProjectAdminPrecondition.new(auth, user))
|
160
|
+
# A step is comprised of a command with 0, 1 or many validators
|
161
|
+
# (e.g. :validators => [...])
|
162
|
+
# The use case can span multiple steps (see below)
|
163
|
+
step(CreateRepositoryCommand.new(user), :validator => NewRepositoryValidator)
|
163
164
|
end
|
164
165
|
end
|
165
166
|
```
|
@@ -170,17 +171,25 @@ This is the high-level overview of how `UseCase` strings up a pipeline
|
|
170
171
|
for you to plug in various kinds of business logic:
|
171
172
|
|
172
173
|
```
|
173
|
-
User input (-> input sanitation) (-> pre-conditions)
|
174
|
+
User input (-> input sanitation) (-> pre-conditions) -> steps
|
174
175
|
```
|
175
176
|
|
176
177
|
1. Start with a hash of user input
|
177
178
|
2. Optionally wrap this in an object that performs type-coercion,
|
178
179
|
enforces types etc.
|
179
180
|
3. Optionally run pre-conditions on the santized input
|
180
|
-
4.
|
181
|
-
|
182
|
-
|
183
|
-
|
181
|
+
4. Execute steps. The initial step is fed the sanitized input, each
|
182
|
+
following command is fed the result from the previous step.
|
183
|
+
|
184
|
+
Each step is a pipeline in its own right:
|
185
|
+
|
186
|
+
```
|
187
|
+
Step: (-> builder) (-> validations) -> command
|
188
|
+
```
|
189
|
+
|
190
|
+
1. Optionally refine input by running it through a pre-execution "builder"
|
191
|
+
2. Optionally run (refined) input through one or more validators
|
192
|
+
3. Execute command with (refined) input
|
184
193
|
|
185
194
|
## Input sanitation
|
186
195
|
|
@@ -222,27 +231,26 @@ dependency. To use this feature, `gem install activemodel`.
|
|
222
231
|
|
223
232
|
## Builders
|
224
233
|
|
225
|
-
When user input has passed input sanitation and pre-conditions have
|
226
|
-
|
227
|
-
|
234
|
+
When user input has passed input sanitation and pre-conditions have been
|
235
|
+
satisfied, you can optionally pipe input through a "builder" before handing it
|
236
|
+
over to validations and a command.
|
228
237
|
|
229
238
|
The builder should be an object with a `build` or a `call` method (if it has
|
230
239
|
both, `build` will be preferred). The method will be called with santized input.
|
231
240
|
The return value will be passed on to validators and the commands.
|
232
241
|
|
233
|
-
Builders can be useful if you want to run validations on a domain
|
234
|
-
|
242
|
+
Builders can be useful if you want to run validations on a domain object rather
|
243
|
+
than directly on "dumb" input.
|
235
244
|
|
236
245
|
### Example
|
237
246
|
|
238
|
-
In a Rails application, the builder is useful to wrap user input in an
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
observers now.
|
247
|
+
In a Rails application, the builder is useful to wrap user input in an unsaved
|
248
|
+
`ActiveRecord` instance. The unsaved object will be run through the validators,
|
249
|
+
and (if found valid), the command can save it and perform additional tasks that
|
250
|
+
you possibly do with `ActiveRecord` observers now.
|
243
251
|
|
244
|
-
This example also shows how to express uniqueness validators when you
|
245
|
-
|
252
|
+
This example also shows how to express uniqueness validators when you move
|
253
|
+
validations out of your `ActiveRecord` models.
|
246
254
|
|
247
255
|
```rb
|
248
256
|
require "activemodel"
|
@@ -289,7 +297,7 @@ class CreateUser
|
|
289
297
|
input_class(NewUserInput)
|
290
298
|
cmd = NewUserCommand.new
|
291
299
|
# Use the command as a builder too
|
292
|
-
|
300
|
+
step(cmd, :builder => cmd, :validator => UserValidator)
|
293
301
|
end
|
294
302
|
end
|
295
303
|
|
@@ -299,19 +307,59 @@ outcome.success? #=> true
|
|
299
307
|
outcome.result #=> #<User name: "Chris">
|
300
308
|
```
|
301
309
|
|
310
|
+
If the command fails to execute due to validation errors, using the builder
|
311
|
+
allows us to access the partial object for re-rendering forms etc. Because this
|
312
|
+
is such a common scenario, the command will automatically be used as the builder
|
313
|
+
as well if there is no explicit `:builder` option, and the command responds to
|
314
|
+
`build`. This means that the command in the previous example could be written as
|
315
|
+
so:
|
316
|
+
|
317
|
+
```rb
|
318
|
+
class CreateUser
|
319
|
+
include UseCase
|
320
|
+
|
321
|
+
def initialize
|
322
|
+
input_class(NewUserInput)
|
323
|
+
step(NewUserCommand.new, :validator => UserValidator)
|
324
|
+
end
|
325
|
+
end
|
326
|
+
```
|
327
|
+
|
328
|
+
When calling `execute` on this use case, we can observe the following flow:
|
329
|
+
|
330
|
+
```rb
|
331
|
+
# This
|
332
|
+
params = { :name => "Dude" }
|
333
|
+
CreateUser.new.execute(params)
|
334
|
+
|
335
|
+
# ...roughly expands to:
|
336
|
+
# (command is the command instance wired in the use case constructor)
|
337
|
+
input = NewUserInput.new(params)
|
338
|
+
prepared = command.build(input)
|
339
|
+
|
340
|
+
if UserValidator.call(prepared).valid?
|
341
|
+
command.execute(prepared)
|
342
|
+
end
|
343
|
+
```
|
344
|
+
|
302
345
|
### Note
|
303
346
|
|
304
|
-
I'm not thrilled by `builder` as a name/concept. Suggestions for a
|
305
|
-
|
347
|
+
I'm not thrilled by `builder` as a name/concept. Suggestions for a better name
|
348
|
+
is welcome.
|
306
349
|
|
307
350
|
## Commands
|
308
351
|
|
309
|
-
A command is any Ruby object that defines an `execute(params)` method.
|
352
|
+
A command is any Ruby object that defines an `execute(params)` method.
|
353
|
+
Alternately, it can be an object that responds to `call` (e.g. a lambda). Its
|
310
354
|
return value will be passed to the outcome's `success` block. Any errors raised
|
311
355
|
by this method is not rescued, so be sure to wrap `use_case.execute(params)` in
|
312
356
|
a rescue block if you're worried that it raises. Better yet, detect known causes
|
313
357
|
of exceptions in a pre-condition so you know that the command does not raise.
|
314
358
|
|
359
|
+
If the command responds to the `build` message and there is no explicitly
|
360
|
+
configured `:builder` for the current step, the command is also used as a
|
361
|
+
builder (see example above, under "Builders").
|
362
|
+
|
315
363
|
## Use cases
|
316
364
|
|
317
365
|
A use case simply glues together all the components. Define a class, include
|
@@ -319,16 +367,16 @@ A use case simply glues together all the components. Define a class, include
|
|
319
367
|
take any arguments you like, making this solution suitable for DI (dependency
|
320
368
|
injection) style designs.
|
321
369
|
|
322
|
-
The use case can optionally call `input_class` once, `
|
323
|
-
times, and `
|
370
|
+
The use case can optionally call `input_class` once, `add_pre_condition`
|
371
|
+
multiple times, and `step` multiple times.
|
324
372
|
|
325
|
-
When using multiple
|
373
|
+
When using multiple steps, input sanitation with the `input_class` is
|
326
374
|
performed once only. Pre-conditions are also only checked once - before any
|
327
|
-
|
375
|
+
steps are executed. The use case will then execute the steps:
|
328
376
|
|
329
377
|
```
|
330
|
-
|
331
|
-
|
378
|
+
step_1: sanitizied_input -> (builder ->) (validators ->) command
|
379
|
+
step_n: command_n-1 result -> (builder ->) (validators ->) command
|
332
380
|
```
|
333
381
|
|
334
382
|
In other words, all commands except the first one will be executed with the
|
data/lib/use_case/version.rb
CHANGED
data/lib/use_case.rb
CHANGED
@@ -31,13 +31,13 @@ module UseCase
|
|
31
31
|
@input_class = input_class
|
32
32
|
end
|
33
33
|
|
34
|
-
def
|
34
|
+
def add_pre_condition(pc)
|
35
35
|
pre_conditions << pc
|
36
36
|
end
|
37
37
|
|
38
|
-
def
|
39
|
-
@
|
40
|
-
@
|
38
|
+
def step(command, options = {})
|
39
|
+
@steps ||= []
|
40
|
+
@steps << {
|
41
41
|
:command => command,
|
42
42
|
:builder => options[:builder],
|
43
43
|
:validators => Array(options[:validators] || options[:validator])
|
@@ -51,23 +51,27 @@ module UseCase
|
|
51
51
|
return outcome
|
52
52
|
end
|
53
53
|
|
54
|
-
|
54
|
+
execute_steps(@steps, input)
|
55
55
|
end
|
56
56
|
|
57
57
|
private
|
58
|
-
def
|
59
|
-
result =
|
58
|
+
def execute_steps(steps, params)
|
59
|
+
result = steps.inject(params) do |input, step|
|
60
60
|
begin
|
61
|
-
input = prepare_input(input,
|
61
|
+
input = prepare_input(input, step)
|
62
62
|
rescue Exception => err
|
63
63
|
return PreConditionFailed.new(self, err)
|
64
64
|
end
|
65
65
|
|
66
|
-
if outcome = validate_params(input,
|
66
|
+
if outcome = validate_params(input, step[:validators])
|
67
67
|
return outcome
|
68
68
|
end
|
69
69
|
|
70
|
-
|
70
|
+
if step[:command].respond_to?(:execute)
|
71
|
+
step[:command].execute(input)
|
72
|
+
else
|
73
|
+
step[:command].call(input)
|
74
|
+
end
|
71
75
|
end
|
72
76
|
|
73
77
|
SuccessfulOutcome.new(self, result)
|
@@ -84,10 +88,11 @@ module UseCase
|
|
84
88
|
nil
|
85
89
|
end
|
86
90
|
|
87
|
-
def prepare_input(input,
|
88
|
-
|
91
|
+
def prepare_input(input, step)
|
92
|
+
builder = step[:builder] || step[:command]
|
89
93
|
return builder.build(input) if builder.respond_to?(:build)
|
90
|
-
builder.call(input)
|
94
|
+
return step[:builder].call(input) if step[:builder]
|
95
|
+
input
|
91
96
|
end
|
92
97
|
|
93
98
|
def validate_params(input, validators)
|
data/test/sample_use_case.rb
CHANGED
@@ -74,9 +74,9 @@ class CreateRepository
|
|
74
74
|
|
75
75
|
def initialize(user)
|
76
76
|
input_class(NewRepositoryInput)
|
77
|
-
|
78
|
-
|
79
|
-
|
77
|
+
add_pre_condition(UserLoggedInPrecondition.new(user))
|
78
|
+
add_pre_condition(ProjectAdminPrecondition.new(user))
|
79
|
+
step(CreateRepositoryCommand.new(user), :validators => NewRepositoryValidator)
|
80
80
|
end
|
81
81
|
end
|
82
82
|
|
@@ -86,7 +86,7 @@ class ExplodingRepository
|
|
86
86
|
def initialize(user)
|
87
87
|
cmd = CreateRepositoryCommand.new(user)
|
88
88
|
def cmd.execute(params); raise "Crash!"; end
|
89
|
-
|
89
|
+
step(cmd)
|
90
90
|
end
|
91
91
|
end
|
92
92
|
|
@@ -104,7 +104,7 @@ class CreateRepositoryWithBuilder
|
|
104
104
|
|
105
105
|
def initialize(user)
|
106
106
|
input_class(NewRepositoryInput)
|
107
|
-
|
107
|
+
step(CreateRepositoryCommand.new(user), {
|
108
108
|
:validators => NewRepositoryValidator,
|
109
109
|
:builder => RepositoryBuilder
|
110
110
|
})
|
@@ -116,13 +116,20 @@ class CreateRepositoryWithExplodingBuilder
|
|
116
116
|
|
117
117
|
def initialize(user)
|
118
118
|
input_class(NewRepositoryInput)
|
119
|
-
|
119
|
+
step(CreateRepositoryCommand.new(user), :builder => self)
|
120
120
|
end
|
121
121
|
|
122
122
|
def build; raise "Oops"; end
|
123
123
|
end
|
124
124
|
|
125
125
|
class PimpRepositoryCommand
|
126
|
+
def execute(repository)
|
127
|
+
repository.name += " (Pimped)"
|
128
|
+
repository
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
class PimpRepositoryCommandWithBuilder
|
126
133
|
def build(repository)
|
127
134
|
repository.id = 42
|
128
135
|
repository
|
@@ -139,8 +146,8 @@ class CreatePimpedRepository
|
|
139
146
|
|
140
147
|
def initialize(user)
|
141
148
|
input_class(NewRepositoryInput)
|
142
|
-
|
143
|
-
|
149
|
+
step(CreateRepositoryCommand.new(user))
|
150
|
+
step(PimpRepositoryCommand.new)
|
144
151
|
end
|
145
152
|
end
|
146
153
|
|
@@ -149,9 +156,9 @@ class CreatePimpedRepository2
|
|
149
156
|
|
150
157
|
def initialize(user)
|
151
158
|
input_class(NewRepositoryInput)
|
152
|
-
|
153
|
-
cmd =
|
154
|
-
|
159
|
+
step(CreateRepositoryCommand.new(user), :builder => RepositoryBuilder)
|
160
|
+
cmd = PimpRepositoryCommandWithBuilder.new
|
161
|
+
step(cmd, :builder => cmd)
|
155
162
|
end
|
156
163
|
end
|
157
164
|
|
@@ -165,8 +172,26 @@ class CreatePimpedRepository3
|
|
165
172
|
|
166
173
|
def initialize(user)
|
167
174
|
input_class(NewRepositoryInput)
|
168
|
-
|
169
|
-
|
170
|
-
|
175
|
+
cmd = PimpRepositoryCommandWithBuilder.new
|
176
|
+
step(CreateRepositoryCommand.new(user), :builder => RepositoryBuilder, :validator => NewRepositoryValidator)
|
177
|
+
step(cmd, :builder => cmd, :validators => [NewRepositoryValidator, PimpedRepositoryValidator])
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
class InlineCommand
|
182
|
+
include UseCase
|
183
|
+
|
184
|
+
def initialize
|
185
|
+
step(lambda { |params| params[:name] })
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
class ImplicitBuilder
|
190
|
+
include UseCase
|
191
|
+
|
192
|
+
def initialize(user)
|
193
|
+
input_class(NewRepositoryInput)
|
194
|
+
step(CreateRepositoryCommand.new(user), :builder => RepositoryBuilder)
|
195
|
+
step(PimpRepositoryCommandWithBuilder.new)
|
171
196
|
end
|
172
197
|
end
|
data/test/use_case_test.rb
CHANGED
@@ -134,4 +134,18 @@ describe UseCase do
|
|
134
134
|
refute outcome.success?
|
135
135
|
assert_equal "You cannot win", outcome.failure.errors[:name].join
|
136
136
|
end
|
137
|
+
|
138
|
+
it "calls command lambda" do
|
139
|
+
outcome = InlineCommand.new.execute({ :name => "Dissection" })
|
140
|
+
|
141
|
+
assert outcome.success?
|
142
|
+
assert_equal "Dissection", outcome.result
|
143
|
+
end
|
144
|
+
|
145
|
+
it "implicitly uses command as builder" do
|
146
|
+
outcome = ImplicitBuilder.new(@logged_in_user).execute({ :name => "Mr" })
|
147
|
+
|
148
|
+
assert_equal 42, outcome.result.id
|
149
|
+
assert_equal "Mr! (Pimped)", outcome.result.name
|
150
|
+
end
|
137
151
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: use_case
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-04-
|
12
|
+
date: 2013-04-05 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: minitest
|