subroutine 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|