subroutine 0.10.0.beta10 → 0.10.0.rc1

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