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 +4 -4
- data/README.md +16 -435
- data/lib/subroutine/association_fields.rb +3 -3
- data/lib/subroutine/fields/configuration.rb +9 -7
- data/lib/subroutine/fields.rb +33 -27
- data/lib/subroutine/version.rb +1 -1
- data/test/subroutine/association_test.rb +16 -3
- data/test/subroutine/base_test.rb +3 -3
- data/test/subroutine/fields_test.rb +11 -0
- data/test/support/ops.rb +30 -6
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 86dd43668cfcb0158dd5c20a1fe87f2d1f4aec05359ed86752cffe81b27c2878
|
4
|
+
data.tar.gz: f83554bfd98877f40037fb528edd35fb36820ebe4566977f0d1fde949392a9d3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
##
|
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
|
-
|
13
|
-
|
14
|
-
|
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.
|
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
|
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
|
-
|
305
|
-
|
306
|
-
|
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
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
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] =
|
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 =
|
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
|
-
|
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
|
data/lib/subroutine/fields.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
44
|
-
|
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
|
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
|
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
|
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 = {})
|
data/lib/subroutine/version.rb
CHANGED
@@ -70,7 +70,7 @@ module Subroutine
|
|
70
70
|
assert_equal "AdminUser", op.admin_type
|
71
71
|
end
|
72
72
|
|
73
|
-
def
|
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
|
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
|
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
|
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
|
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
|
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
|
-
|
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
|
-
|
124
|
+
fields_from ::DefaultsOp, except: %i[foo bar]
|
125
125
|
|
126
126
|
end
|
127
127
|
|
128
128
|
class OnlyFooBarOp < ::Subroutine::Op
|
129
129
|
|
130
|
-
|
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
|
-
|
307
|
+
fields_from SimpleAssociationOp
|
302
308
|
|
303
309
|
end
|
304
310
|
|
305
311
|
class InheritedUnscopedAssociation < ::Subroutine::Op
|
306
312
|
|
307
|
-
|
313
|
+
fields_from UnscopedSimpleAssociationOp
|
308
314
|
|
309
315
|
end
|
310
316
|
|
311
317
|
class InheritedPolymorphicAssociationOp < ::Subroutine::Op
|
312
318
|
|
313
|
-
|
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.
|
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-
|
11
|
+
date: 2020-01-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|