subroutine 0.1.0 → 0.1.1
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 +148 -107
- data/lib/subroutine/op.rb +15 -7
- data/lib/subroutine/version.rb +1 -1
- data/test/subroutine/base_test.rb +1 -1
- data/test/support/ops.rb +2 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9cd32153de5f8bf76c3a0c0ad71bd48d195cf0c3
|
4
|
+
data.tar.gz: 7b5045cedc5c4e544e179ab3d3f45e64e9607a28
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 91b76020520dc64668f1d8494cbc44daa98bdc116a597eae5b7aaabe135b25d3918cfbbdeb0696c4bd073ecf4b23527e8876a02a7f676d34c27a01821c4a6ab7
|
7
|
+
data.tar.gz: 13454289c41dc9e69dd0a3d2cbb2e42f9ac211a46bc80ac2129db1adc2070b47218b2dd29962d752ccbd940473c93fe0f8dd6ad8e950642f4d428f4ba734ba64
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Subroutine
|
2
2
|
|
3
|
-
A gem that provides an interface for creating feature-driven operations. It
|
3
|
+
A gem that provides an interface for creating feature-driven operations. It utilizes the command pattern, enables the usage of "ops" as "form objects", and just all-around enables clear, concise, meaningful code.
|
4
4
|
|
5
5
|
## Examples
|
6
6
|
|
@@ -32,7 +32,7 @@ class SignupOp < ::Subroutine::Op
|
|
32
32
|
end
|
33
33
|
|
34
34
|
def build_user
|
35
|
-
User.new(
|
35
|
+
User.new(params)
|
36
36
|
end
|
37
37
|
|
38
38
|
def deliver_welcome_email!(u)
|
@@ -76,21 +76,26 @@ app/
|
|
76
76
|
```
|
77
77
|
|
78
78
|
#### Model
|
79
|
+
|
80
|
+
When ops are around, the point of the model is to ensure data validity. That's essentially it.
|
81
|
+
So most of your models are a series of validations, common accessors, queries, etc.
|
82
|
+
|
79
83
|
```ruby
|
80
|
-
# When ops are around, the point of the model is to ensure the data entering the db is 100% valid.
|
81
|
-
# So most of your models are a series of validations and common accessors, queries, etc.
|
82
84
|
class User
|
83
|
-
validates :name,
|
84
|
-
validates :email,
|
85
|
+
validates :name, presence: true
|
86
|
+
validates :email, email: true
|
85
87
|
|
86
88
|
has_secure_password
|
87
89
|
end
|
88
90
|
```
|
89
91
|
|
90
92
|
#### Controller(s)
|
93
|
+
|
94
|
+
I've found that a great way to handle errors with ops is to allow you top level controller to appropriately
|
95
|
+
render errors in a consisent way. This is exceptionally easy for api-driven apps.
|
96
|
+
|
97
|
+
|
91
98
|
```ruby
|
92
|
-
# I've found that a great way to handle errors with ops is to allow you top level controller to appropriately
|
93
|
-
# render errors in a consisent way. This is exceptionally easy for api-driven apps.
|
94
99
|
class Api::Controller < ApplicationController
|
95
100
|
rescue_from ::Subroutine::Failure, with: :render_op_failure
|
96
101
|
|
@@ -99,10 +104,14 @@ class Api::Controller < ApplicationController
|
|
99
104
|
# e.record.errors, etc
|
100
105
|
end
|
101
106
|
end
|
107
|
+
```
|
102
108
|
|
103
|
-
|
109
|
+
With ops, your controllers are essentially just connections between routes, operations, and templates.
|
110
|
+
|
111
|
+
```ruby
|
104
112
|
class UsersController < ::Api::Controller
|
105
113
|
def sign_up
|
114
|
+
|
106
115
|
# If the op fails, a ::Subroutine::Failure will be raised.
|
107
116
|
op = SignupOp.submit!(params)
|
108
117
|
|
@@ -111,146 +120,186 @@ class UsersController < ::Api::Controller
|
|
111
120
|
end
|
112
121
|
end
|
113
122
|
```
|
123
|
+
## Op Implementation
|
114
124
|
|
115
|
-
|
125
|
+
Ops have some fluff, but not much. The `Subroutine::Op` class entire purpose in life is to validate user input and execute
|
126
|
+
a series of operations. To enable this we filter input params, type cast params (if desired), and execute validations. Only
|
127
|
+
after these things are complete will the `Op` perform it's operation.
|
116
128
|
|
117
|
-
|
129
|
+
#### Input Declaration
|
118
130
|
|
119
|
-
|
131
|
+
Inputs are declared via the `field` method and have just a couple of options:
|
120
132
|
|
121
133
|
```ruby
|
122
|
-
|
123
|
-
|
134
|
+
class MyOp < ::Subroutine::Op
|
135
|
+
field :first_name
|
136
|
+
field :age, type: :integer
|
137
|
+
field :subscribed, type: :boolean, default: false
|
138
|
+
# ...
|
139
|
+
end
|
124
140
|
```
|
125
141
|
|
126
|
-
|
142
|
+
* **type** - declares the type which the input should be cast to. Available types are declared in `Subroutine::TypeCaster::TYPES`
|
143
|
+
* **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.
|
144
|
+
* **aka** - an alias (or aliases) that is checked when errors are inherited from other objects.
|
145
|
+
|
146
|
+
Since we like a clean & simple dsl, you can also declare inputs via the `values` of `Subroutine::TypeCaster::TYPES`. When declared
|
147
|
+
this way, the `:type` option is assumed.
|
127
148
|
|
128
149
|
```ruby
|
129
|
-
|
130
|
-
|
131
|
-
|
150
|
+
class MyOp < ::Subroutine::Op
|
151
|
+
string :first_name
|
152
|
+
date :dob
|
153
|
+
boolean :tos, :default => false
|
154
|
+
end
|
132
155
|
|
133
|
-
####
|
156
|
+
#### Validations
|
157
|
+
|
158
|
+
Since Ops inlcude ActiveModel::Model, validations can be used just like any other ActiveModel object.
|
134
159
|
|
135
160
|
```ruby
|
136
|
-
|
137
|
-
|
138
|
-
|
161
|
+
class MyOp < ::Subroutine::Op
|
162
|
+
field :first_name
|
163
|
+
|
164
|
+
validates :first_name, presence: true
|
165
|
+
end
|
139
166
|
```
|
140
167
|
|
141
|
-
####
|
168
|
+
#### Input Usage
|
169
|
+
|
170
|
+
Inputs are accessible within the op via public accessors. You can see if an input was provided via the `field_provided?` method.
|
142
171
|
|
143
172
|
```ruby
|
144
|
-
|
145
|
-
op.submit!
|
146
|
-
# if the op succeeds nothing will be raised, otherwise a ::Subroutine::Failure will be raised.
|
147
|
-
```
|
173
|
+
class MyOp < ::Subroutine::Op
|
148
174
|
|
149
|
-
|
175
|
+
field :first_name
|
176
|
+
validate :validate_first_name_is_not_bob
|
150
177
|
|
151
|
-
|
178
|
+
protected
|
152
179
|
|
153
|
-
|
154
|
-
|
180
|
+
def perform
|
181
|
+
# whatever this op does
|
182
|
+
true
|
183
|
+
end
|
155
184
|
|
156
|
-
|
157
|
-
|
158
|
-
inputs_from ::SignupOp
|
185
|
+
def validate_first_name_is_not_bob
|
186
|
+
return true unless field_provided?(:first_name)
|
159
187
|
|
160
|
-
|
161
|
-
|
162
|
-
|
188
|
+
if first_name.downcase == 'bob'
|
189
|
+
errors.add(:first_name, 'should not be bob')
|
190
|
+
return false
|
191
|
+
end
|
163
192
|
|
164
|
-
|
165
|
-
|
166
|
-
|
193
|
+
true
|
194
|
+
end
|
195
|
+
end
|
196
|
+
```
|
167
197
|
|
168
|
-
|
169
|
-
# default thank_you_message: "Thanks so much"
|
170
|
-
# field thank_you_message: "Thanks so much"
|
171
|
-
# field :thank_you_message, default: "Thanks so much"
|
198
|
+
#### Execution
|
172
199
|
|
173
|
-
|
174
|
-
|
200
|
+
Every op must implement a `perform` instance method. This is the method which will be executed if all validations pass.
|
201
|
+
The return value of this op determines whether the operation was a success or not. Truthy values are assumed to be successful,
|
202
|
+
while falsy values are assumed to be failures. In general, returning `true` at the end of the perform method is desired.
|
175
203
|
|
176
|
-
|
177
|
-
|
178
|
-
|
204
|
+
```ruby
|
205
|
+
class MyOp < ::Subroutine::Op
|
206
|
+
field :first_name
|
207
|
+
validates :first_name, presence: true
|
179
208
|
|
180
209
|
protected
|
181
210
|
|
182
|
-
# This is where the actual operation takes place.
|
183
211
|
def perform
|
184
|
-
|
212
|
+
$logger.info "#{first_name} submitted this op"
|
213
|
+
true
|
214
|
+
end
|
185
215
|
|
186
|
-
|
187
|
-
|
188
|
-
user = create_user!
|
189
|
-
associate_invitation!(user)
|
190
|
-
end
|
216
|
+
end
|
217
|
+
```
|
191
218
|
|
192
|
-
|
193
|
-
@activated_user = user
|
219
|
+
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!`.
|
194
220
|
|
195
|
-
|
196
|
-
true
|
197
|
-
end
|
221
|
+
#### Errors
|
198
222
|
|
199
|
-
|
200
|
-
# You have access to the original inputs via original_params
|
201
|
-
def create_user!
|
202
|
-
op = ::SignupOp.submit!(original_params)
|
203
|
-
op.signed_up_user
|
204
|
-
end
|
223
|
+
Reporting errors is very important in Subroutine Ops since these can be used as form objects. Errors can be reported a couple different ways:
|
205
224
|
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
_invitation.convert!
|
211
|
-
end
|
225
|
+
1) `errors.add(:key, :error)` That is, the way you add errors to an ActiveModel object. Then either return false from your op OR raise an error like `raise ::Subroutine::Failure.new(this)`.
|
226
|
+
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,
|
227
|
+
it checks whether the key in the provided error_object matches a field (or aka of a field) in our op. If there is a match, the error will be placed on
|
228
|
+
that field, but if there is not, the error will be placed on `:base`. Again, after adding the errors to our op, we must return `false` from the perform method or raise a Subroutine::Failure.
|
212
229
|
|
213
|
-
|
214
|
-
|
215
|
-
# You can check to see if a specific field was provided via field_provided?()
|
216
|
-
return thank_you_message if field_provided?(:thank_you_message)
|
217
|
-
thank_you_message.presence || I18n.t('thanks')
|
218
|
-
end
|
230
|
+
```ruby
|
231
|
+
class MyOp < ::Subroutine::Op
|
219
232
|
|
220
|
-
|
221
|
-
|
222
|
-
return @_invitation if defined?(@_invitation)
|
223
|
-
@_invitation = token ? ::Invitation.find_by(token: token) : nil
|
224
|
-
end
|
233
|
+
string :first_name, aka: :firstname
|
234
|
+
string :last_name, aka: [:lastname, :surname]
|
225
235
|
|
226
|
-
|
227
|
-
# In most cases, these validations can be written simpler.
|
228
|
-
# The true/false return value is a style I like but not required.
|
229
|
-
def validate_invitation_available
|
236
|
+
protected
|
230
237
|
|
231
|
-
|
232
|
-
return true if token.blank?
|
238
|
+
def perform
|
233
239
|
|
234
|
-
|
235
|
-
|
236
|
-
unless _invitation.present?
|
237
|
-
errors.add(:token, :not_found)
|
240
|
+
if first_name == 'bill'
|
241
|
+
errors.add(:first_name, 'cannot be bill')
|
238
242
|
return false
|
239
243
|
end
|
240
244
|
|
241
|
-
|
242
|
-
|
243
|
-
|
245
|
+
if first_name == 'john'
|
246
|
+
errors.add(:first_name, 'cannot be john')
|
247
|
+
raise Subroutine::Failure.new(this)
|
248
|
+
end
|
249
|
+
|
250
|
+
unless _user.valid?
|
251
|
+
|
252
|
+
# if there are :first_name or :firstname errors on _user, they will be added to our :first_name
|
253
|
+
# if there are :last_name, :lastname, or :surname errors on _user, they will be added to our :last_name
|
254
|
+
inherit_errors(_user)
|
244
255
|
return false
|
245
256
|
end
|
246
257
|
|
247
258
|
true
|
248
259
|
end
|
249
260
|
|
261
|
+
def _user
|
262
|
+
@_user ||= User.new(params)
|
263
|
+
end
|
250
264
|
end
|
251
265
|
```
|
252
266
|
|
253
|
-
|
267
|
+
|
268
|
+
## Usage
|
269
|
+
|
270
|
+
The `Subroutine::Op` class' `submit` and `submit!` methods have identical signatures to the class' constructor, enabling a few different ways to utilize an op:
|
271
|
+
|
272
|
+
#### Via the class' `submit` method
|
273
|
+
|
274
|
+
```ruby
|
275
|
+
op = MyOp.submit({foo: 'bar'})
|
276
|
+
# if the op succeeds it will be returned, otherwise it false will be returned.
|
277
|
+
```
|
278
|
+
|
279
|
+
#### Via the class' `submit!` method
|
280
|
+
|
281
|
+
```ruby
|
282
|
+
op = MyOp.submit!({foo: 'bar'})
|
283
|
+
# if the op succeeds it will be returned, otherwise a ::Subroutine::Failure will be raised.
|
284
|
+
```
|
285
|
+
|
286
|
+
#### Via the instance's `submit` method
|
287
|
+
|
288
|
+
```ruby
|
289
|
+
op = MyOp.new({foo: 'bar'})
|
290
|
+
val = op.submit
|
291
|
+
# if the op succeeds, val will be true, otherwise false
|
292
|
+
```
|
293
|
+
|
294
|
+
#### Via the instance's `submit!` method
|
295
|
+
|
296
|
+
```ruby
|
297
|
+
op = MyOp.new({foo: 'bar'})
|
298
|
+
op.submit!
|
299
|
+
# if the op succeeds nothing will be raised, otherwise a ::Subroutine::Failure will be raised.
|
300
|
+
```
|
301
|
+
|
302
|
+
## Extending Subroutine::Op
|
254
303
|
|
255
304
|
Great, so you're sold on using ops. Let's talk about how I usually standardize their usage in my apps. The most common thing needed is `current_user`. For this reason I usually follow the rails convention of declaring an "Application" op which declares all of my common needs. I hate writing `ApplicationOp` all the time so I usually call it `BaseOp`.
|
256
305
|
|
@@ -268,7 +317,7 @@ class BaseOp < ::Subroutine::Op
|
|
268
317
|
end
|
269
318
|
```
|
270
319
|
|
271
|
-
|
320
|
+
So now I can pass the current user as my first argument to any op constructor. The next most common case is permissions. In a common role-based system things become pretty easy. I usually just add a class method which declares the minimum required role.
|
272
321
|
|
273
322
|
```ruby
|
274
323
|
class SendInvitationOp < BaseOp
|
@@ -344,11 +393,3 @@ end
|
|
344
393
|
|
345
394
|
1. Enable ActiveModel 3.0-3.2 users by removing the ActiveModel::Model dependency.
|
346
395
|
2. Demo app?
|
347
|
-
|
348
|
-
## Contributing
|
349
|
-
|
350
|
-
1. Fork it ( https://github.com/[my-github-username]/subroutine/fork )
|
351
|
-
2. Create your feature branch (`git checkout -b my-new-feature`)
|
352
|
-
3. Commit your changes (`git commit -am 'Add some feature'`)
|
353
|
-
4. Push to the branch (`git push origin my-new-feature`)
|
354
|
-
5. Create a new Pull Request
|
data/lib/subroutine/op.rb
CHANGED
@@ -52,6 +52,7 @@ module Subroutine
|
|
52
52
|
def inherited(child)
|
53
53
|
super
|
54
54
|
child._fields = self._fields.dup
|
55
|
+
child._error_map = self._error_map.dup
|
55
56
|
end
|
56
57
|
|
57
58
|
|
@@ -73,6 +74,12 @@ module Subroutine
|
|
73
74
|
def _field(field_name, options = {})
|
74
75
|
self._fields[field_name.to_sym] = options
|
75
76
|
|
77
|
+
if options[:aka]
|
78
|
+
Array(options[:aka]).each do |as|
|
79
|
+
self._error_map[as.to_sym] = field_name.to_sym
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
76
83
|
class_eval <<-EV, __FILE__, __LINE__ + 1
|
77
84
|
|
78
85
|
def #{field_name}=(v)
|
@@ -102,6 +109,9 @@ module Subroutine
|
|
102
109
|
class_attribute :_fields
|
103
110
|
self._fields = {}
|
104
111
|
|
112
|
+
class_attribute :_error_map
|
113
|
+
self._error_map = {}
|
114
|
+
|
105
115
|
attr_reader :original_params
|
106
116
|
attr_reader :params
|
107
117
|
|
@@ -132,7 +142,7 @@ module Subroutine
|
|
132
142
|
rescue Exception => e
|
133
143
|
|
134
144
|
if e.respond_to?(:record)
|
135
|
-
|
145
|
+
inherit_errors(e.record) unless e.record == self
|
136
146
|
false
|
137
147
|
else
|
138
148
|
raise e
|
@@ -181,19 +191,17 @@ module Subroutine
|
|
181
191
|
@params.has_key?(key)
|
182
192
|
end
|
183
193
|
|
184
|
-
# applies the errors to the form object from the child object
|
185
|
-
def inherit_errors_from(object)
|
186
|
-
inherit_errors(object.errors)
|
187
|
-
end
|
188
|
-
|
189
|
-
|
190
194
|
# applies the errors in error_object to self
|
191
195
|
# returns false so failure cases can end with this invocation
|
192
196
|
def inherit_errors(error_object)
|
197
|
+
error_object = error_object.errors if error_object.respond_to?(:errors)
|
198
|
+
|
193
199
|
error_object.each do |k,v|
|
194
200
|
|
195
201
|
if respond_to?("#{k}")
|
196
202
|
errors.add(k, v)
|
203
|
+
elsif self._error_map[k.to_sym]
|
204
|
+
errors.add(self._error_map[k.to_sym], v)
|
197
205
|
else
|
198
206
|
errors.add(:base, error_object.full_message(k,v))
|
199
207
|
end
|
data/lib/subroutine/version.rb
CHANGED
@@ -93,7 +93,7 @@ module Subroutine
|
|
93
93
|
assert op.perform_called
|
94
94
|
refute op.perform_finished
|
95
95
|
|
96
|
-
assert_equal ["
|
96
|
+
assert_equal ["has gotta be @admin.com"], op.errors[:email]
|
97
97
|
end
|
98
98
|
|
99
99
|
def test_when_valid_perform_completes_it_returns_control
|
data/test/support/ops.rb
CHANGED
@@ -18,7 +18,7 @@ end
|
|
18
18
|
|
19
19
|
class SignupOp < ::Subroutine::Op
|
20
20
|
|
21
|
-
string :email
|
21
|
+
string :email, :aka => :email_address
|
22
22
|
string :password
|
23
23
|
|
24
24
|
validates :email, :presence => true
|
@@ -36,7 +36,7 @@ class SignupOp < ::Subroutine::Op
|
|
36
36
|
u = build_user
|
37
37
|
|
38
38
|
unless u.valid?
|
39
|
-
|
39
|
+
inherit_errors(u)
|
40
40
|
return false
|
41
41
|
end
|
42
42
|
|
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.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Nelson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-05-
|
11
|
+
date: 2015-05-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|