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 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: