subroutine 0.10.0.beta10 → 0.10.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 70efa6b815728e1b2d8e3117ebc40b629d98f7f150528a1901ff7e03f763fa10
4
- data.tar.gz: 28d84cad0e8508b32c0f7ad93d3c79b94a3cdef9b5632c3b933f7f85a04b703b
3
+ metadata.gz: 86dd43668cfcb0158dd5c20a1fe87f2d1f4aec05359ed86752cffe81b27c2878
4
+ data.tar.gz: f83554bfd98877f40037fb528edd35fb36820ebe4566977f0d1fde949392a9d3
5
5
  SHA512:
6
- metadata.gz: 3d2f21c4f6739a3ad32c5915f7f8eb60efece410ceb78b31f76b1f5f522ee65b669c0f42b4d5c43bd5aedf549cefe7643e837879e1cf513676491017aa8a6bf1
7
- data.tar.gz: '06698f7f7ddd4993ee0e6651a8f1b38f665fc770be023d532bd7b5671030213f26f58b18cf58c1d569433f6ce41854b98909f94bbd15b1ba8a5261e97a77d512'
6
+ metadata.gz: '0395205fa3d3ea249e531f5ad21399f53b34a26919ae712c2bf2432c2d611760ed5b72f290f70603a9d8c686f547cc9809d7dc3a7d04ce52848af3a5a226bf57'
7
+ data.tar.gz: b48edaa5cdca4a844c22536ad75984646e4bb7ac11ebd06758041d90bda67402e594ad9e00025089831fbeffebb09e67d86f58f3aa7772c84056bbef639dfa34
data/README.md CHANGED
@@ -1,17 +1,17 @@
1
1
  # Subroutine
2
2
 
3
- A gem that provides an interface for creating feature-driven operations. You've probably heard at least one of these terms: "service objects", "form objects", or maybe even "commands". Subroutine calls these "ops" and really it's just about enabling clear, concise, testable, and meaningful code.
3
+ A gem that provides an interface for creating feature-driven operations. You've probably heard at least one of these terms: "service objects", "form objects", "intentions", or "commands". Subroutine calls these "ops" and really it's just about enabling clear, concise, testable, and meaningful code.
4
4
 
5
- ## Examples
5
+ ## Example
6
6
 
7
7
  So you need to sign up a user? or maybe update one's account? or change a password? or maybe you need to sign up a business along with a user, associate them, send an email, and queue a worker in a single request? Not a problem, create an op for any of these use cases. Here's the signup example.
8
8
 
9
9
  ```ruby
10
10
  class SignupOp < ::Subroutine::Op
11
11
 
12
- field :name
13
- field :email
14
- field :password
12
+ string :name
13
+ string :email
14
+ string :password
15
15
 
16
16
  validates :name, presence: true
17
17
  validates :email, presence: true
@@ -28,8 +28,8 @@ class SignupOp < ::Subroutine::Op
28
28
  output :signed_up_user, u
29
29
  end
30
30
 
31
- def create_user
32
- User.new(params)
31
+ def create_user!
32
+ User.create!(params)
33
33
  end
34
34
 
35
35
  def deliver_welcome_email!(u)
@@ -38,433 +38,14 @@ class SignupOp < ::Subroutine::Op
38
38
  end
39
39
  ```
40
40
 
41
- So why is this needed?
42
-
43
- 1. No insane cluttering of controllers with strong parameters, etc.
44
- 2. No insane cluttering of models with validations, callbacks, and random methods that don't relate to integrity or access of model data.
45
- 3. Insanely testable.
46
- 4. Insanely easy to read and maintain.
47
- 5. Multi-model operations become insanely easy.
48
- 6. Your sanity.
49
-
50
- ### Connecting it all
51
-
52
- ```txt
53
- app/
54
- |
55
- |- controllers/
56
- | |- users_controller.rb
57
- |
58
- |- models/
59
- | |- user.rb
60
- |
61
- |- ops/
62
- |- signup_op.rb
63
-
64
- ```
65
-
66
- #### Route
67
- ```ruby
68
- resources :users, only: [] do
69
- collection do
70
- post :signup
71
- end
72
- end
73
- ```
74
-
75
- #### Model
76
-
77
- When ops are around, the point of the model is to ensure data validity. That's essentially it.
78
- So most of your models are a series of validations, common accessors, queries, etc.
79
-
80
- ```ruby
81
- class User
82
- validates :name, presence: true
83
- validates :email, email: true
84
-
85
- has_secure_password
86
- end
87
- ```
88
-
89
- #### Controller(s)
90
-
91
- I've found that a great way to handle errors with ops is to allow you top level controller to appropriately
92
- render errors in a consisent way. This is exceptionally easy for api-driven apps.
93
-
94
-
95
- ```ruby
96
- class Api::Controller < ApplicationController
97
- rescue_from ::Subroutine::Failure, with: :render_op_failure
98
-
99
- def render_op_failure(e)
100
- # however you want to do this, `e` will be similar to an ActiveRecord::RecordInvalid error
101
- # e.record.errors, etc
102
- end
103
- end
104
- ```
105
-
106
- With ops, your controllers are essentially just connections between routes, operations, and whatever you use to build responses.
107
-
108
- ```ruby
109
- class UsersController < ::Api::Controller
110
- def sign_up
111
-
112
- # If the op fails, a ::Subroutine::Failure will be raised.
113
- op = SignupOp.submit!(params)
114
-
115
- # If the op succeeds, it will be returned so you can access it's information.
116
- render json: op.signed_up_user
117
- end
118
- end
119
- ```
120
- ## Op Implementation
121
-
122
- Ops have some fluff, but not much. The `Subroutine::Op` class' entire purpose in life is to validate user input and execute
123
- a series of operations. To enable this we filter input params, type cast params (if desired), and execute validations. Only
124
- after these things are complete will the `Op` perform it's operation.
125
-
126
- #### Input Declaration
127
-
128
- Inputs are declared via the `field` method and have just a couple of options:
129
-
130
- ```ruby
131
- class MyOp < ::Subroutine::Op
132
- field :first_name
133
- field :age, type: :integer
134
- field :subscribed, type: :boolean, default: false
135
- # ...
136
- end
137
- ```
138
-
139
- * **type** - declares the type which the input should be cast to. Available types are declared in `Subroutine::TypeCaster::TYPES`
140
- * **default** - the default value of the input if not otherwise provided. If the provided default responds to `call` (ie. proc, lambda) the result of that `call` will be used at runtime.
141
- * **aka** - an alias (or aliases) that is checked when errors are inherited from other objects.
142
-
143
- Since we like a clean & simple dsl, you can also declare inputs via the `values` of `Subroutine::TypeCaster::TYPES`. When declared
144
- this way, the `:type` option is assumed.
145
-
146
- ```ruby
147
- class MyOp < ::Subroutine::Op
148
- string :first_name
149
- date :dob
150
- boolean :tos, :default => false
151
- end
152
- ```
153
-
154
- Since ops can use other ops, sometimes it's nice to explicitly state the inputs are valid. To "inherit" all the inputs from another op, simply use `inputs_from`.
155
-
156
- ```ruby
157
- class MyOp < ::Subroutine::Op
158
- string :token
159
- inputs_from MyOtherOp
160
-
161
- protected
162
-
163
- def perform
164
- verify_token!
165
- MyOtherOp.submit! params.except(:token)
166
- end
167
-
168
- end
169
- ```
170
-
171
- #### Validations
172
-
173
- Since Ops include ActiveModel::Model, validations can be used just like any other ActiveModel object.
174
-
175
- ```ruby
176
- class MyOp < ::Subroutine::Op
177
- field :first_name
178
-
179
- validates :first_name, presence: true
180
- end
181
- ```
182
-
183
- #### Input Usage
184
-
185
- Inputs are accessible within the op via public accessors. You can see if an input was provided via the `field_provided?` method.
186
-
187
- ```ruby
188
- class MyOp < ::Subroutine::Op
189
-
190
- field :first_name
191
- validate :validate_first_name_is_not_bob
192
-
193
- protected
194
-
195
- def perform
196
- # whatever this op does
197
- true
198
- end
199
-
200
- def validate_first_name_is_not_bob
201
- if field_provided?(:first_name) && first_name.downcase == 'bob'
202
- errors.add(:first_name, 'should not be bob')
203
- end
204
- end
205
- end
206
- ```
207
-
208
- All **provided** params are accessible via the `params` accessor. All default values are accessible via the `defaults` accessor. The combination of the two is available via `params_with_defaults`.
209
-
210
- ```ruby
211
- class MyOp < ::Subroutine::Op
212
- string :name
213
- string :status, default: "browsing"
214
-
215
- def perform
216
- puts params.inspect
217
- puts defaults.inspect
218
- puts params_with_defaults.inspect
219
- end
220
- end
221
-
222
- MyOp.submit(name: "foobar", status: nil)
223
- # => { name: "foobar" }
224
- # => { status: "browsing" }
225
- # => { name: "foobar", status: nil }
226
-
227
- MyOp.submit(name: "foobar")
228
- # => { name: "foobar" }
229
- # => { status: "browsing" }
230
- # => { name: "foobar", status: "browsing" }
231
- ```
232
-
233
- #### Execution
234
-
235
- Every op must implement a `perform` method. This is the method which will be executed if all validations pass.
236
- When the the `perform` method is complete, the Op determins success based on whether `errors` is empty.
237
-
238
- ```ruby
239
- class MyFailingOp < ::Subroutine::Op
240
- field :first_name
241
- validates :first_name, presence: true
242
-
243
- protected
244
-
245
- def perform
246
- errors.add(:base, "This will never succeed")
247
- end
248
-
249
- end
250
- ```
251
-
252
- Notice we do not declare `perform` as a public method. This is to ensure the "public" api of the op remains as `submit` or `submit!`.
253
-
254
- #### Errors
255
-
256
- Reporting errors is very important in Subroutine Ops since these can be used as form objects. Errors can be reported a couple different ways:
257
-
258
- 1. `errors.add(:key, :error)` That is, the way you add errors to an ActiveModel object.
259
- 2. `inherit_errors(error_object_or_activemodel_object)` Same as `errors.add`, but it iterates an existing error hash and inherits the errors. As part of this iteration,
260
- it checks whether the key in the provided error_object matches a field (or alias of a field) in our op. If there is a match, the error will be placed on
261
- that field, but if there is not, the error will be placed on `:base`.
262
-
263
- ```ruby
264
- class MyOp < ::Subroutine::Op
265
-
266
- string :first_name, aka: :firstname
267
- string :last_name, aka: [:lastname, :surname]
268
-
269
- protected
270
-
271
- def perform
272
-
273
- if first_name == 'bill'
274
- errors.add(:first_name, 'cannot be bill')
275
- return
276
- end
277
-
278
- if first_name == 'john'
279
- errors.add(:first_name, 'cannot be john')
280
- return
281
- end
282
-
283
- unless _user.valid?
284
-
285
- # if there are :first_name or :firstname errors on _user, they will be added to our :first_name
286
- # if there are :last_name, :lastname, or :surname errors on _user, they will be added to our :last_name
287
- inherit_errors(_user)
288
- end
289
- end
290
-
291
- def _user
292
- @_user ||= User.new(params)
293
- end
294
- end
295
- ```
296
-
297
-
298
- ## Usage
299
-
300
- The `Subroutine::Op` class' `submit` and `submit!` methods have identical signatures to the class' constructor, enabling a few different ways to utilize an op:
301
-
302
- #### Via the class' `submit` method
41
+ ## So why use this?
303
42
 
304
- ```ruby
305
- op = MyOp.submit({foo: 'bar'})
306
- # if the op succeeds it will be returned, otherwise false will be returned.
307
- ```
308
-
309
- #### Via the class' `submit!` method
310
-
311
- ```ruby
312
- op = MyOp.submit!({foo: 'bar'})
313
- # if the op succeeds it will be returned, otherwise a ::Subroutine::Failure will be raised.
314
- ```
43
+ - Avoid cluttering models or controllers with logic only applicable to one intention. You also don't need strong parameters because the inputs to the Op are well-defined.
44
+ - Test the Op in isolation
45
+ - Clear and concise intention in a single file
46
+ - Multi-model operations become simple
315
47
 
316
- #### Via the instance's `submit` method
317
-
318
- ```ruby
319
- op = MyOp.new({foo: 'bar'})
320
- val = op.submit
321
- # if the op succeeds, val will be true, otherwise false
322
- ```
323
-
324
- #### Via the instance's `submit!` method
325
-
326
- ```ruby
327
- op = MyOp.new({foo: 'bar'})
328
- op.submit!
329
- # if the op succeeds nothing will be raised, otherwise a ::Subroutine::Failure will be raised.
330
- ```
331
-
332
- ## Built-in Extensions
333
-
334
- ### Subroutine::Association
335
-
336
- The `Subroutine::Association` module provides an interface for loading ActiveRecord instances easily.
337
-
338
- ```ruby
339
- class UserUpdateOp < ::Subroutine::Op
340
- include ::Subroutine::Association
341
-
342
- association :user
343
-
344
- string :first_name, :last_name
345
-
346
- protected
347
-
348
- def perform
349
- user.update_attributes(
350
- first_name: first_name,
351
- last_name: last_name
352
- )
353
- end
354
- end
355
- ```
356
-
357
- ```ruby
358
- class RecordTouchOp < ::Subroutine::Op
359
- include ::Subroutine::Association
360
-
361
- association :record, polymorphic: true
362
-
363
- protected
364
-
365
- def perform
366
- record.touch
367
- end
368
- end
369
- ```
370
-
371
- ### Subroutine::Auth
372
-
373
- The `Subroutine::Auth` module provides basic bindings for application authorization. It assumes that, optionally, a `User` will be provided as the first argument to an Op. It forces authorization to be declared on each class it's included in.
374
-
375
- ```ruby
376
- class SayHiOp < ::Subroutine::Op
377
- include ::Subroutine::Auth
378
-
379
- require_user!
380
-
381
- string :say_what, default: "hi"
382
-
383
- protected
384
-
385
- def perform
386
- puts "#{current_user.name} says: #{say_what}"
387
- end
388
- end
389
- ```
390
-
391
- ```ruby
392
- user = User.find("john")
393
- SayHiOp.submit!(user)
394
- # => John says: hi
395
-
396
- SayHiOp.submit!(user, say_what: "hello")
397
- # => John says: hello
398
-
399
-
400
- SayHiOp.submit!
401
- # => raises Subroutine::Auth::NotAuthorizedError
402
- ```
403
-
404
- There are a handful of authorization configurations:
405
-
406
- 1. `require_user!` - ensures that a user is provided
407
- 2. `require_no_user!` - ensures that a user is not present
408
- 3. `no_user_requirements!` - explicitly doesn't matter
409
-
410
- In addition to these top-level authorization declarations you can provide custom authorizations like so:
411
-
412
- ```ruby
413
- class AccountSetSecretOp < ::Subroutine::Op
414
- include ::Subroutine::Auth
415
-
416
- require_user!
417
- authorize :authorize_first_name_is_john
418
-
419
- # If you use a policy-based authorization framework like pundit:
420
- # `policy` is a shortcut for the following:
421
- # authorize -> { unauthorized! unless policy.can_set_secret? }
422
-
423
- policy :can_set_secret?
424
-
425
- string :secret
426
- belongs_to :account
427
-
428
- protected
429
-
430
- def perform
431
- account.secret = secret
432
- current_user.save!
433
- end
434
-
435
- def authorize_first_name_is_john
436
- unless current_user.first_name == "john"
437
- unauthorized!
438
- end
439
- end
440
-
441
- def policy
442
- ::UserPolicy.new(current_user, current_user)
443
- end
444
-
445
- end
446
- ```
447
-
448
- ## Subroutine::Factory
449
-
450
- There is a separate gem [subroutine-factory](https://github.com/mnelson/subroutine-factory) which enables you to easily utilize factories and operations to produce
451
- test data. It's a great replacement to FactoryGirl, as it ensures the data entering your DB is getting there via a real
452
- world operation.
453
-
454
- ```ruby
455
- # support/factories/signups.rb
456
- Subroutine::Factory.define :signup do
457
- op ::SignupOp
458
-
459
- inputs :email, sequence{|n| "foo{n}@example.com" }
460
- inputs :password, "password123"
461
-
462
- # by default, the op will be returned when the factory is used.
463
- # this `output` returns the value of the `user` output on the resulting op
464
- output :user
465
- end
466
-
467
- # signup_test.rb
468
- user = Subroutine::Factory.create :signup
469
- user = Subroutine::Factory.create :signup, email: "foo@bar.com"
470
- ```
48
+ [Implementing an Op](https://github.com/guideline-tech/subroutine/wiki/Implementing-an-Op)
49
+ [Using an Op](https://github.com/guideline-tech/subroutine/wiki/Using-an-Op)
50
+ [Errors](https://github.com/guideline-tech/subroutine/wiki/Errors)
51
+ [Basic Usage in Rails](https://github.com/guideline-tech/subroutine/wiki/Rails-Usage)
@@ -105,7 +105,7 @@ module Subroutine
105
105
 
106
106
  out = params.except(*excepts)
107
107
  association_fields.each_pair do |field_name, _config|
108
- out[field_name] = send(field_name) if field_provided?(field_name)
108
+ out[field_name] = get_field(field_name) if field_provided?(field_name)
109
109
  end
110
110
 
111
111
  out
@@ -135,8 +135,8 @@ module Subroutine
135
135
  stored_result = association_cache[config.field_name]
136
136
  return stored_result unless stored_result.nil?
137
137
 
138
- fk = send(config.foreign_key_method)
139
- type = send(config.foreign_type_method)
138
+ fk = get_field(config.foreign_key_method)
139
+ type = config.polymorphic? ? get_field(config.foreign_type_method) : send(config.foreign_type_method)
140
140
 
141
141
  result = fetch_association_instance(type, fk, config.unscoped?)
142
142
  association_cache[config.field_name] = result
@@ -7,7 +7,8 @@ module Subroutine
7
7
  class Configuration < ::SimpleDelegator
8
8
 
9
9
  PROTECTED_GROUP_IDENTIFIERS = %i[all original default].freeze
10
- INHERITABLE_OPTIONS = %i[mass_assignable field_reader field_writer].freeze
10
+ INHERITABLE_OPTIONS = %i[mass_assignable field_reader field_writer groups].freeze
11
+ NO_GROUPS = [].freeze
11
12
 
12
13
  def self.from(field_name, options)
13
14
  case options
@@ -29,6 +30,10 @@ module Subroutine
29
30
 
30
31
  alias config __getobj__
31
32
 
33
+ def merge(options = {})
34
+ self.class.new(field_name, config.merge(options))
35
+ end
36
+
32
37
  def required_modules
33
38
  []
34
39
  end
@@ -69,12 +74,8 @@ module Subroutine
69
74
  config[:field_reader] != false
70
75
  end
71
76
 
72
- def parent_field
73
- config[:parent_field]
74
- end
75
-
76
77
  def groups
77
- config[:groups]
78
+ config[:groups] || NO_GROUPS
78
79
  end
79
80
 
80
81
  def in_group?(group_name)
@@ -92,7 +93,8 @@ module Subroutine
92
93
  def sanitize_options(options)
93
94
  opts = (options || {}).to_h.dup
94
95
  groups = opts[:group] || opts[:groups]
95
- opts[:groups] = Array(groups).map(&:to_sym)
96
+ groups = nil if groups == false
97
+ opts[:groups] = Array(groups).map(&:to_sym).presence
96
98
  opts.delete(:group)
97
99
  opts
98
100
  end
@@ -17,6 +17,9 @@ module Subroutine
17
17
  included do
18
18
  class_attribute :field_configurations
19
19
  self.field_configurations = {}
20
+
21
+ class_attribute :field_groups
22
+ self.field_groups = Set.new
20
23
  end
21
24
 
22
25
  module ClassMethods
@@ -25,35 +28,19 @@ module Subroutine
25
28
  config = ::Subroutine::Fields::Configuration.from(field_name, options)
26
29
  config.validate!
27
30
 
28
- config.groups.each do |group_name|
29
- _group(group_name)
30
- end
31
-
32
31
  self.field_configurations = field_configurations.merge(field_name.to_sym => config)
33
32
 
34
- if config.field_writer?
35
- class_eval <<-EV, __FILE__, __LINE__ + 1
36
- try(:silence_redefinition_of_method, :#{field_name}=)
37
- def #{field_name}=(v)
38
- set_field(:#{field_name}, v)
39
- end
40
- EV
41
- end
33
+ ensure_field_accessors(config)
42
34
 
43
- if config.field_reader?
44
- class_eval <<-EV, __FILE__, __LINE__ + 1
45
- try(:silence_redefinition_of_method, :#{field_name})
46
- def #{field_name}
47
- get_field(:#{field_name})
48
- end
49
- EV
35
+ config.groups.each do |group_name|
36
+ ensure_group_accessors(group_name)
50
37
  end
51
38
 
52
39
  config
53
40
  end
54
41
  alias input field
55
42
 
56
- def inputs_from(*things)
43
+ def fields_from(*things)
57
44
  options = things.extract_options!
58
45
  excepts = options.key?(:except) ? Array(options.delete(:except)) : nil
59
46
  onlys = options.key?(:only) ? Array(options.delete(:only)) : nil
@@ -67,11 +54,11 @@ module Subroutine
67
54
  include mod unless included_modules.include?(mod)
68
55
  end
69
56
 
70
- field(field_name, config)
57
+ field(field_name, config.merge(options))
71
58
  end
72
59
  end
73
60
  end
74
- alias fields_from inputs_from
61
+ alias inputs_from fields_from
75
62
 
76
63
  def fields_in_group(group_name)
77
64
  field_configurations.each_with_object({}) do |(field_name, config), h|
@@ -103,31 +90,50 @@ module Subroutine
103
90
 
104
91
  protected
105
92
 
106
- def _group(group_name)
93
+ def ensure_group_accessors(group_name)
94
+ group_name = group_name.to_sym
95
+ return if field_groups.include?(group_name)
96
+
97
+ self.field_groups |= [group_name]
98
+
107
99
  class_eval <<-EV, __FILE__, __LINE__ + 1
108
- try(:silence_redefinition_of_method, :#{group_name}_params)
109
100
  def #{group_name}_params
110
101
  param_groups[:#{group_name}]
111
102
  end
112
103
 
113
- try(:silence_redefinition_of_method, :#{group_name}_default_params)
114
104
  def #{group_name}_default_params
115
105
  group_field_names = fields_in_group(:#{group_name}).keys
116
106
  all_default_params.slice(*group_field_names)
117
107
  end
118
108
 
119
- try(:silence_redefinition_of_method, :#{group_name}_params_with_defaults)
120
109
  def #{group_name}_params_with_defaults
121
110
  #{group_name}_default_params.merge(param_groups[:#{group_name}])
122
111
  end
123
112
 
124
- try(:silence_redefinition_of_method, :without_#{group_name}_params)
125
113
  def without_#{group_name}_params
126
114
  all_params.except(*#{group_name}_params.keys)
127
115
  end
128
116
  EV
129
117
  end
130
118
 
119
+ def ensure_field_accessors(config)
120
+ if config.field_writer? && !instance_methods.include?(:"#{config.field_name}=")
121
+ class_eval <<-EV, __FILE__, __LINE__ + 1
122
+ def #{config.field_name}=(v)
123
+ set_field(:#{config.field_name}, v)
124
+ end
125
+ EV
126
+ end
127
+
128
+ if config.field_reader? && !instance_methods.include?(:"#{config.field_name}")
129
+ class_eval <<-EV, __FILE__, __LINE__ + 1
130
+ def #{config.field_name}
131
+ get_field(:#{config.field_name})
132
+ end
133
+ EV
134
+ end
135
+ end
136
+
131
137
  end
132
138
 
133
139
  def setup_fields(inputs = {})
@@ -5,7 +5,7 @@ module Subroutine
5
5
  MAJOR = 0
6
6
  MINOR = 10
7
7
  PATCH = 0
8
- PRE = "beta10"
8
+ PRE = "rc1"
9
9
 
10
10
  VERSION = [MAJOR, MINOR, PATCH, PRE].compact.join(".")
11
11
 
@@ -70,7 +70,7 @@ module Subroutine
70
70
  assert_equal "AdminUser", op.admin_type
71
71
  end
72
72
 
73
- def test_it_inherits_associations_via_inputs_from
73
+ def test_it_inherits_associations_via_fields_from
74
74
  all_mock = mock
75
75
 
76
76
  ::User.expects(:all).returns(all_mock)
@@ -82,7 +82,7 @@ module Subroutine
82
82
  assert_equal doug.id, op.user_id
83
83
  end
84
84
 
85
- def test_it_inherits_associations_via_inputs_from_and_preserves_options
85
+ def test_it_inherits_associations_via_fields_from_and_preserves_options
86
86
  all_mock = mock
87
87
  unscoped_mock = mock
88
88
 
@@ -96,7 +96,7 @@ module Subroutine
96
96
  assert_equal doug.id, op.user_id
97
97
  end
98
98
 
99
- def test_it_inherits_polymorphic_associations_via_inputs_from
99
+ def test_it_inherits_polymorphic_associations_via_fields_from
100
100
  all_mock = mock
101
101
  ::User.expects(:all).never
102
102
  ::AdminUser.expects(:all).returns(all_mock)
@@ -210,5 +210,18 @@ module Subroutine
210
210
  assert_equal({ "admin" => user }, op.params_with_associations)
211
211
  end
212
212
 
213
+ def test_groups_are_preserved_to_association_components
214
+ user = ::User.new(id: 1)
215
+ op = GroupedParamAssociationOp.new(user: user)
216
+ assert_equal({ "user_id" => 1 }, op.params)
217
+ assert_equal({ "user_id" => 1 }, op.info_params)
218
+ assert_equal({}, op.without_info_params)
219
+
220
+ op = GroupedPolymorphicParamAssociationOp.new(user: user)
221
+ assert_equal({ "user_id" => 1, "user_type" => "User" }, op.params)
222
+ assert_equal({ "user_id" => 1, "user_type" => "User" }, op.info_params)
223
+ assert_equal({}, op.without_info_params)
224
+ end
225
+
213
226
  end
214
227
  end
@@ -24,7 +24,7 @@ module Subroutine
24
24
  refute_equal sid, bid
25
25
  end
26
26
 
27
- def test_inputs_from_inherited_fields_without_inheriting_from_the_class
27
+ def test_fields_from_inherited_fields_without_inheriting_from_the_class
28
28
  refute ::BusinessSignupOp < ::SignupOp
29
29
 
30
30
  user_fields = ::SignupOp.field_configurations.keys
@@ -35,14 +35,14 @@ module Subroutine
35
35
  end
36
36
  end
37
37
 
38
- def test_inputs_from_ignores_except_fields
38
+ def test_fields_from_ignores_except_fields
39
39
  op = ::ExceptFooBarOp.new
40
40
  refute op.field_configurations.key?(:foo)
41
41
  refute op.field_configurations.key?(:bar)
42
42
  assert_equal [:baz], op.field_configurations.keys.sort
43
43
  end
44
44
 
45
- def test_inputs_from_only_fields
45
+ def test_fields_from_only_fields
46
46
  op = ::OnlyFooBarOp.new
47
47
  assert op.field_configurations.key?(:foo)
48
48
  assert op.field_configurations.key?(:bar)
@@ -127,5 +127,16 @@ module Subroutine
127
127
  assert_equal({ foo: "bar" }.with_indifferent_access, op.ungrouped_params)
128
128
  end
129
129
 
130
+ def test_fields_from_allows_merging_of_config
131
+ op = GroupedDefaultsOp.new(foo: "foo")
132
+ assert_equal({ foo: "foo" }.with_indifferent_access, op.params)
133
+ assert_equal({ foo: "foo" }.with_indifferent_access, op.inherited_params)
134
+ assert_equal({ foo: "foo", bar: "bar", baz: false }.with_indifferent_access, op.params_with_defaults)
135
+ assert_equal({ foo: "foo", bar: "bar", baz: false }.with_indifferent_access, op.inherited_params_with_defaults)
136
+ assert_equal({}.with_indifferent_access, op.without_inherited_params)
137
+ end
138
+
139
+ def test_groups_can_be_removed_via_fields_from; end
140
+
130
141
  end
131
142
  end
data/test/support/ops.rb CHANGED
@@ -107,7 +107,7 @@ class BusinessSignupOp < ::Subroutine::Op
107
107
 
108
108
  string :business_name
109
109
 
110
- inputs_from ::SignupOp
110
+ fields_from ::SignupOp
111
111
 
112
112
  end
113
113
 
@@ -121,13 +121,13 @@ end
121
121
 
122
122
  class ExceptFooBarOp < ::Subroutine::Op
123
123
 
124
- inputs_from ::DefaultsOp, except: %i[foo bar]
124
+ fields_from ::DefaultsOp, except: %i[foo bar]
125
125
 
126
126
  end
127
127
 
128
128
  class OnlyFooBarOp < ::Subroutine::Op
129
129
 
130
- inputs_from ::DefaultsOp, only: %i[foo bar]
130
+ fields_from ::DefaultsOp, only: %i[foo bar]
131
131
 
132
132
  end
133
133
 
@@ -137,6 +137,12 @@ class InheritedDefaultsOp < ::DefaultsOp
137
137
 
138
138
  end
139
139
 
140
+ class GroupedDefaultsOp < ::Subroutine::Op
141
+
142
+ fields_from ::DefaultsOp, group: "inherited"
143
+
144
+ end
145
+
140
146
  class TypeCastOp < ::Subroutine::Op
141
147
 
142
148
  integer :integer_input
@@ -298,19 +304,37 @@ end
298
304
 
299
305
  class InheritedSimpleAssociation < ::Subroutine::Op
300
306
 
301
- inputs_from SimpleAssociationOp
307
+ fields_from SimpleAssociationOp
302
308
 
303
309
  end
304
310
 
305
311
  class InheritedUnscopedAssociation < ::Subroutine::Op
306
312
 
307
- inputs_from UnscopedSimpleAssociationOp
313
+ fields_from UnscopedSimpleAssociationOp
308
314
 
309
315
  end
310
316
 
311
317
  class InheritedPolymorphicAssociationOp < ::Subroutine::Op
312
318
 
313
- inputs_from PolymorphicAssociationOp
319
+ fields_from PolymorphicAssociationOp
320
+
321
+ end
322
+
323
+ class GroupedParamAssociationOp < ::OpWithAssociation
324
+
325
+ association :user, group: :info
326
+
327
+ end
328
+
329
+ class GroupedPolymorphicParamAssociationOp < ::OpWithAssociation
330
+
331
+ association :user, polymorphic: true, group: :info
332
+
333
+ end
334
+
335
+ class GroupedInputsFromOp < ::Subroutine::Op
336
+
337
+ fields_from GroupedParamAssociationOp, group: :inherited
314
338
 
315
339
  end
316
340
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: subroutine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0.beta10
4
+ version: 0.10.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Nelson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-01-28 00:00:00.000000000 Z
11
+ date: 2020-01-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel