use_case 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile.lock CHANGED
@@ -12,7 +12,7 @@ GIT
12
12
  PATH
13
13
  remote: .
14
14
  specs:
15
- use_case (0.4.0)
15
+ use_case (0.6.0)
16
16
 
17
17
  GEM
18
18
  remote: http://rubygems.org/
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
- # Multiple validators can be added if needed
161
- validator(NewRepositoryValidator)
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 (-> command...)
173
+ User input (-> input sanitation) (-> pre-conditions) [(-> builder) (-> validations) -> command]*
174
174
  ```
175
175
 
176
- * Start with a hash of user input
177
- * Optionally wrap this in an object that performs type-coercion,
178
- enforces types etc. By default, input will be wrapped in an `OpenStruct`
179
- * Optionally run pre-conditions on the santized input
180
- * Optionally refine input by running it through a pre-execution "builder"
181
- * Optionally (refined) input through one or more validators
182
- * Execute command(s) with (refined) input
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. The main difference is that the validator is created as a stand-alone
209
- object that can be used with any model instance. This design allows you to
210
- define multiple context-sensitive validations for a single object.
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 Datamapper project closely in this area.
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 the commands.
227
+ before handing it over to validations and a command.
225
228
 
226
- The builder should be an object with a `build` method. The method will
227
- be called with santized input. The return value from `build` will be
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
- builder(cmd) # Use the command as a builder too
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 `validator` multiple times. It *must* call `command` once with the
326
- command object.
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
 
@@ -24,5 +24,5 @@
24
24
  #++
25
25
 
26
26
  module UseCase
27
- VERSION = "0.5.0"
27
+ VERSION = "0.6.0"
28
28
  end
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 << command
45
- end
46
-
47
- def builder(builder)
48
- @builder = builder
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
- begin
59
- input = @builder.build(input) if @builder
60
- rescue Exception => err
61
- return PreConditionFailed.new(self, err)
62
- end
54
+ execute_commands(@commands, input)
55
+ end
63
56
 
64
- if outcome = validate_params(input)
65
- return outcome
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 validate_params(input)
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
@@ -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
- validator(NewRepositoryValidator)
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
- builder(RepositoryBuilder)
109
- validator(NewRepositoryValidator)
110
- command(CreateRepositoryCommand.new(user))
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(self)
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 execute(repository); repository.name += " (Pimped)"; repository end
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
- def build; raise "Oops"; end
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
@@ -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
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.5.0
4
+ version: 0.6.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors: