use_case 0.5.0 → 0.6.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/Gemfile.lock +1 -1
- data/Readme.md +38 -29
- data/lib/use_case/version.rb +1 -1
- data/lib/use_case.rb +29 -21
- data/test/sample_use_case.rb +41 -10
- data/test/use_case_test.rb +14 -0
- metadata +1 -1
data/Gemfile.lock
CHANGED
data/Readme.md
CHANGED
@@ -30,7 +30,7 @@ The following example is a simplified use case from
|
|
30
30
|
do this, we need a user that can admin the project under which we want the new
|
31
31
|
repository to live.
|
32
32
|
|
33
|
-
This example illustrates how to solve common design challenges in Rails
|
33
|
+
_NB!_ This example illustrates how to solve common design challenges in Rails
|
34
34
|
applications; that does not mean that `UseCase` is only useful to Rails
|
35
35
|
applications.
|
36
36
|
|
@@ -157,9 +157,9 @@ class CreateRepository
|
|
157
157
|
input_class(NewRepositoryInput)
|
158
158
|
pre_condition(UserLoggedInPrecondition.new(user))
|
159
159
|
pre_condition(ProjectAdminPrecondition.new(auth, user))
|
160
|
-
#
|
161
|
-
|
162
|
-
command(CreateRepositoryCommand.new(user))
|
160
|
+
# A command has 0, 1 or many validators (e.g. :validators => [...])
|
161
|
+
# The use case can span multiple commands (see below)
|
162
|
+
command(CreateRepositoryCommand.new(user), :validator => NewRepositoryValidator)
|
163
163
|
end
|
164
164
|
end
|
165
165
|
```
|
@@ -170,16 +170,17 @@ This is the high-level overview of how `UseCase` strings up a pipeline
|
|
170
170
|
for you to plug in various kinds of business logic:
|
171
171
|
|
172
172
|
```
|
173
|
-
User input (-> input sanitation) (-> pre-conditions) (-> builder) (-> validations) -> command
|
173
|
+
User input (-> input sanitation) (-> pre-conditions) [(-> builder) (-> validations) -> command]*
|
174
174
|
```
|
175
175
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
176
|
+
1. Start with a hash of user input
|
177
|
+
2. Optionally wrap this in an object that performs type-coercion,
|
178
|
+
enforces types etc.
|
179
|
+
3. Optionally run pre-conditions on the santized input
|
180
|
+
4. Optionally refine input by running it through a pre-execution "builder"
|
181
|
+
5. Optionally run (refined) input through one or more validators
|
182
|
+
6. Execute command(s) with (refined) input
|
183
|
+
7. Repeat steps 4-7 as necessary
|
183
184
|
|
184
185
|
## Input sanitation
|
185
186
|
|
@@ -205,13 +206,15 @@ pre-condition instance that failed.
|
|
205
206
|
## Validations
|
206
207
|
|
207
208
|
The validator uses `ActiveModel::Validations`, so any Rails validation can go in
|
208
|
-
here
|
209
|
-
|
210
|
-
|
209
|
+
here (except for `validates_uniqueness_of`, which apparently comes from
|
210
|
+
elsewhere - see example below for how to work around this). The main difference
|
211
|
+
is that the validator is created as a stand-alone object that can be used with
|
212
|
+
any model instance. This design allows you to define multiple context-sensitive
|
213
|
+
validations for a single object.
|
211
214
|
|
212
215
|
You can of course provide your own validation if you want - any object that
|
213
216
|
defines `call(object)` and returns something that responds to `valid?` is good.
|
214
|
-
I am following the
|
217
|
+
I am following the Datamapper2 project closely in this area.
|
215
218
|
|
216
219
|
Because `UseCase::Validation` is not a required part of `UseCase`, and people
|
217
220
|
may want to control their own dependencies, `activemodel` is _not_ a hard
|
@@ -221,11 +224,11 @@ dependency. To use this feature, `gem install activemodel`.
|
|
221
224
|
|
222
225
|
When user input has passed input sanitation and pre-conditions have
|
223
226
|
been satisfied, you can optionally pipe input through a "builder"
|
224
|
-
before handing it over to validations and
|
227
|
+
before handing it over to validations and a command.
|
225
228
|
|
226
|
-
The builder should be an object with a `build`
|
227
|
-
|
228
|
-
passed on to validators and the commands.
|
229
|
+
The builder should be an object with a `build` or a `call` method (if it has
|
230
|
+
both, `build` will be preferred). The method will be called with santized input.
|
231
|
+
The return value will be passed on to validators and the commands.
|
229
232
|
|
230
233
|
Builders can be useful if you want to run validations on a domain
|
231
234
|
object rather than directly on "dumb" input.
|
@@ -284,10 +287,9 @@ class CreateUser
|
|
284
287
|
|
285
288
|
def initialize
|
286
289
|
input_class(NewUserInput)
|
287
|
-
validator(UserValidator)
|
288
290
|
cmd = NewUserCommand.new
|
289
|
-
|
290
|
-
command(cmd)
|
291
|
+
# Use the command as a builder too
|
292
|
+
command(cmd, :builder => cmd, :validator => UserValidator)
|
291
293
|
end
|
292
294
|
end
|
293
295
|
|
@@ -310,10 +312,6 @@ by this method is not rescued, so be sure to wrap `use_case.execute(params)` in
|
|
310
312
|
a rescue block if you're worried that it raises. Better yet, detect known causes
|
311
313
|
of exceptions in a pre-condition so you know that the command does not raise.
|
312
314
|
|
313
|
-
A use case can execute multiple commands. When you do, the result of the first
|
314
|
-
command will be the input to the second command and so on. The result of the
|
315
|
-
last command will be the final `outcome.result`.
|
316
|
-
|
317
315
|
## Use cases
|
318
316
|
|
319
317
|
A use case simply glues together all the components. Define a class, include
|
@@ -322,8 +320,19 @@ take any arguments you like, making this solution suitable for DI (dependency
|
|
322
320
|
injection) style designs.
|
323
321
|
|
324
322
|
The use case can optionally call `input_class` once, `pre_condition` multiple
|
325
|
-
times, and `
|
326
|
-
|
323
|
+
times, and `command` multiple times.
|
324
|
+
|
325
|
+
When using multiple commands, input sanitation with the `input_class` is
|
326
|
+
performed once only. Pre-conditions are also only checked once - before any
|
327
|
+
commands are executed. The use case will then execute the commands:
|
328
|
+
|
329
|
+
```
|
330
|
+
command_1: sanitizied_input -> (builder ->) (validators ->) command
|
331
|
+
command_n: command_n-1 result -> (builder ->) (validators ->) command
|
332
|
+
```
|
333
|
+
|
334
|
+
In other words, all commands except the first one will be executed with the
|
335
|
+
result of the previous command as input.
|
327
336
|
|
328
337
|
## Outcomes
|
329
338
|
|
data/lib/use_case/version.rb
CHANGED
data/lib/use_case.rb
CHANGED
@@ -31,21 +31,17 @@ module UseCase
|
|
31
31
|
@input_class = input_class
|
32
32
|
end
|
33
33
|
|
34
|
-
def validator(validator)
|
35
|
-
validators << validator
|
36
|
-
end
|
37
|
-
|
38
34
|
def pre_condition(pc)
|
39
35
|
pre_conditions << pc
|
40
36
|
end
|
41
37
|
|
42
|
-
def command(command)
|
38
|
+
def command(command, options = {})
|
43
39
|
@commands ||= []
|
44
|
-
@commands <<
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
40
|
+
@commands << {
|
41
|
+
:command => command,
|
42
|
+
:builder => options[:builder],
|
43
|
+
:validators => Array(options[:validators] || options[:validator])
|
44
|
+
}
|
49
45
|
end
|
50
46
|
|
51
47
|
def execute(params)
|
@@ -55,21 +51,28 @@ module UseCase
|
|
55
51
|
return outcome
|
56
52
|
end
|
57
53
|
|
58
|
-
|
59
|
-
|
60
|
-
rescue Exception => err
|
61
|
-
return PreConditionFailed.new(self, err)
|
62
|
-
end
|
54
|
+
execute_commands(@commands, input)
|
55
|
+
end
|
63
56
|
|
64
|
-
|
65
|
-
|
57
|
+
private
|
58
|
+
def execute_commands(commands, params)
|
59
|
+
result = commands.inject(params) do |input, command|
|
60
|
+
begin
|
61
|
+
input = prepare_input(input, command[:builder])
|
62
|
+
rescue Exception => err
|
63
|
+
return PreConditionFailed.new(self, err)
|
64
|
+
end
|
65
|
+
|
66
|
+
if outcome = validate_params(input, command[:validators])
|
67
|
+
return outcome
|
68
|
+
end
|
69
|
+
|
70
|
+
command[:command].execute(input)
|
66
71
|
end
|
67
72
|
|
68
|
-
result = @commands.inject(input) { |input, command| command.execute(input) }
|
69
73
|
SuccessfulOutcome.new(self, result)
|
70
74
|
end
|
71
75
|
|
72
|
-
private
|
73
76
|
def verify_pre_conditions(input)
|
74
77
|
pre_conditions.each do |pc|
|
75
78
|
begin
|
@@ -81,7 +84,13 @@ module UseCase
|
|
81
84
|
nil
|
82
85
|
end
|
83
86
|
|
84
|
-
def
|
87
|
+
def prepare_input(input, builder)
|
88
|
+
return input if !builder
|
89
|
+
return builder.build(input) if builder.respond_to?(:build)
|
90
|
+
builder.call(input)
|
91
|
+
end
|
92
|
+
|
93
|
+
def validate_params(input, validators)
|
85
94
|
validators.each do |validator|
|
86
95
|
result = validator.call(input)
|
87
96
|
return FailedOutcome.new(self, result) if !result.valid?
|
@@ -90,5 +99,4 @@ module UseCase
|
|
90
99
|
end
|
91
100
|
|
92
101
|
def pre_conditions; @pre_conditions ||= []; end
|
93
|
-
def validators; @validators ||= []; end
|
94
102
|
end
|
data/test/sample_use_case.rb
CHANGED
@@ -76,8 +76,7 @@ class CreateRepository
|
|
76
76
|
input_class(NewRepositoryInput)
|
77
77
|
pre_condition(UserLoggedInPrecondition.new(user))
|
78
78
|
pre_condition(ProjectAdminPrecondition.new(user))
|
79
|
-
|
80
|
-
command(CreateRepositoryCommand.new(user))
|
79
|
+
command(CreateRepositoryCommand.new(user), :validators => NewRepositoryValidator)
|
81
80
|
end
|
82
81
|
end
|
83
82
|
|
@@ -105,9 +104,10 @@ class CreateRepositoryWithBuilder
|
|
105
104
|
|
106
105
|
def initialize(user)
|
107
106
|
input_class(NewRepositoryInput)
|
108
|
-
|
109
|
-
|
110
|
-
|
107
|
+
command(CreateRepositoryCommand.new(user), {
|
108
|
+
:validators => NewRepositoryValidator,
|
109
|
+
:builder => RepositoryBuilder
|
110
|
+
})
|
111
111
|
end
|
112
112
|
end
|
113
113
|
|
@@ -116,16 +116,22 @@ class CreateRepositoryWithExplodingBuilder
|
|
116
116
|
|
117
117
|
def initialize(user)
|
118
118
|
input_class(NewRepositoryInput)
|
119
|
-
builder
|
120
|
-
validator(NewRepositoryValidator)
|
121
|
-
command(CreateRepositoryCommand.new(user))
|
119
|
+
command(CreateRepositoryCommand.new(user), :builder => self)
|
122
120
|
end
|
123
121
|
|
124
122
|
def build; raise "Oops"; end
|
125
123
|
end
|
126
124
|
|
127
125
|
class PimpRepositoryCommand
|
128
|
-
def
|
126
|
+
def build(repository)
|
127
|
+
repository.id = 42
|
128
|
+
repository
|
129
|
+
end
|
130
|
+
|
131
|
+
def execute(repository)
|
132
|
+
repository.name += " (Pimped)"
|
133
|
+
repository
|
134
|
+
end
|
129
135
|
end
|
130
136
|
|
131
137
|
class CreatePimpedRepository
|
@@ -136,6 +142,31 @@ class CreatePimpedRepository
|
|
136
142
|
command(CreateRepositoryCommand.new(user))
|
137
143
|
command(PimpRepositoryCommand.new)
|
138
144
|
end
|
145
|
+
end
|
139
146
|
|
140
|
-
|
147
|
+
class CreatePimpedRepository2
|
148
|
+
include UseCase
|
149
|
+
|
150
|
+
def initialize(user)
|
151
|
+
input_class(NewRepositoryInput)
|
152
|
+
command(CreateRepositoryCommand.new(user), :builder => RepositoryBuilder)
|
153
|
+
cmd = PimpRepositoryCommand.new
|
154
|
+
command(cmd, :builder => cmd)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
PimpedRepositoryValidator = UseCase::Validator.define do
|
159
|
+
validate :cannot_win
|
160
|
+
def cannot_win; errors.add(:name, "You cannot win"); end
|
161
|
+
end
|
162
|
+
|
163
|
+
class CreatePimpedRepository3
|
164
|
+
include UseCase
|
165
|
+
|
166
|
+
def initialize(user)
|
167
|
+
input_class(NewRepositoryInput)
|
168
|
+
command(CreateRepositoryCommand.new(user), :builder => RepositoryBuilder, :validator => NewRepositoryValidator)
|
169
|
+
cmd = PimpRepositoryCommand.new
|
170
|
+
command(cmd, :builder => cmd, :validators => [NewRepositoryValidator, PimpedRepositoryValidator])
|
171
|
+
end
|
141
172
|
end
|
data/test/use_case_test.rb
CHANGED
@@ -120,4 +120,18 @@ describe UseCase do
|
|
120
120
|
assert_equal 1349, outcome.result.id
|
121
121
|
assert_equal "Mr (Pimped)", outcome.result.name
|
122
122
|
end
|
123
|
+
|
124
|
+
it "chains two commands with individual builders" do
|
125
|
+
outcome = CreatePimpedRepository2.new(@logged_in_user).execute({ :name => "Mr" })
|
126
|
+
|
127
|
+
assert_equal 42, outcome.result.id
|
128
|
+
assert_equal "Mr! (Pimped)", outcome.result.name
|
129
|
+
end
|
130
|
+
|
131
|
+
it "fails one of three validators" do
|
132
|
+
outcome = CreatePimpedRepository3.new(@logged_in_user).execute({ :name => "Mr" })
|
133
|
+
|
134
|
+
refute outcome.success?
|
135
|
+
assert_equal "You cannot win", outcome.failure.errors[:name].join
|
136
|
+
end
|
123
137
|
end
|