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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 55319d241f3bd11613cff48ead9bae0355dfe8fa
4
- data.tar.gz: 147c53874dfa950fb987b31b4b9742f0f3a7e7f1
3
+ metadata.gz: 9cd32153de5f8bf76c3a0c0ad71bd48d195cf0c3
4
+ data.tar.gz: 7b5045cedc5c4e544e179ab3d3f45e64e9607a28
5
5
  SHA512:
6
- metadata.gz: 697e9e97d7161ac52db1c226ffa7f4e665d90d3c4f1b1fe58c1ae6e6d98555ae1a82538985b6b85c8690a4eef7385e74530068f054417cd34c45ff40a9a4f46c
7
- data.tar.gz: 72303c67318d9aba48903ed4cf6ea9595adf22004f5af775f59a1df9d0082d08af1ed6954cd19b165d288669e58802b3ab1a391c30c0f36272d0e50e55e08d0f
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 loosely implements the command pattern if you're interested in nerding out a bit. See the examples below, it'll be more clear.
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(filtered_params)
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, presence: true
84
- validates :email, email: true
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
- # With ops, your controllers are essentially just connections between routes, operations, and templates.
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
- ## Usage
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
- The `Subroutine::Op` class' `submit` and `submit!` methods have the same signature as the class' constructor, enabling a few different ways to utilize an op. Here they are:
129
+ #### Input Declaration
118
130
 
119
- #### Via the class' `submit` method
131
+ Inputs are declared via the `field` method and have just a couple of options:
120
132
 
121
133
  ```ruby
122
- op = MyOp.submit({foo: 'bar'})
123
- # if the op succeeds it will be returned, otherwise it false will be returned.
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
- #### Via the class' `submit!` method
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
- op = MyOp.submit!({foo: 'bar'})
130
- # if the op succeeds it will be returned, otherwise a ::Subroutine::Failure will be raised.
131
- ```
150
+ class MyOp < ::Subroutine::Op
151
+ string :first_name
152
+ date :dob
153
+ boolean :tos, :default => false
154
+ end
132
155
 
133
- #### Via the instance's `submit` method
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
- op = MyOp.new({foo: 'bar'})
137
- val = op.submit
138
- # if the op succeeds, val will be true, otherwise false
161
+ class MyOp < ::Subroutine::Op
162
+ field :first_name
163
+
164
+ validates :first_name, presence: true
165
+ end
139
166
  ```
140
167
 
141
- #### Via the instance's `submit!` method
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
- op = MyOp.new({foo: 'bar'})
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
- #### Fluff
175
+ field :first_name
176
+ validate :validate_first_name_is_not_bob
150
177
 
151
- Ops have some fluff. Let's see if we can cover it all with one example. I'll pretend I'm using ActiveRecord:
178
+ protected
152
179
 
153
- ```ruby
154
- class ActivateOp < ::Subroutine::Op
180
+ def perform
181
+ # whatever this op does
182
+ true
183
+ end
155
184
 
156
- # This will inherit all fields, error mappings, and default values from the SignupOp class.
157
- # It currently does not inherit validations
158
- inputs_from ::SignupOp
185
+ def validate_first_name_is_not_bob
186
+ return true unless field_provided?(:first_name)
159
187
 
160
- # This defines new inputs for this op.
161
- field :invitation_token
162
- field :thank_you_message
188
+ if first_name.downcase == 'bob'
189
+ errors.add(:first_name, 'should not be bob')
190
+ return false
191
+ end
163
192
 
164
- # This maps any "inherited" errors to the op's input.
165
- # So if one of our objects that we inherit errors from has an email_address error, it will end up on our errors as "email".
166
- error_map email_address: :email
193
+ true
194
+ end
195
+ end
196
+ ```
167
197
 
168
- # If you wanted default values, they can be declared a couple different ways:
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
- # If your default values need to be evaluated at runtime, simply wrap them in a proc:
174
- # default thank_you_message: -> { I18n.t('thank_you') }
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
- # Validations are declared just like any other ActiveModel
177
- validates :token, presence: true
178
- validate :validate_invitation_available
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
- user = nil
212
+ $logger.info "#{first_name} submitted this op"
213
+ true
214
+ end
185
215
 
186
- # Jump into a transaction to make sure any failure rolls back all changes.
187
- ActiveRecord::Base.transaction do
188
- user = create_user!
189
- associate_invitation!(user)
190
- end
216
+ end
217
+ ```
191
218
 
192
- # Set our "success" accessors.
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
- # Return a truthy value to declare success.
196
- true
197
- end
221
+ #### Errors
198
222
 
199
- # Use an existing op! OMG SO DRY
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
- # Deal with our invitation after our user is saved.
207
- def associate_invitation!(user)
208
- _invitation.user_id = user.id
209
- _invitation.thank_you_message = defaulted_thank_you_message
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
- # Build a default value if the user didn't provide one.
214
- def defaulted_thank_you_message
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
- # Fetch the invitation via the provided token.
221
- def _invitation
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
- # Verbosely validate the existence of the invitation.
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
- # The other validation has already added a message for a blank token.
232
- return true if token.blank?
238
+ def perform
233
239
 
234
- # Ensure we found an invitation matching the token.
235
- # We could have used find_by!() in `_invitation` as well.
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
- # Ensure the token is valid.
242
- unless _invitation.can_be_converted?
243
- errors.add(:token, :not_convertable)
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
- ### Extending Subroutine::Op
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
- Great, 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.
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
- inherit_errors_from(e.record) unless e.record == self
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
@@ -2,7 +2,7 @@ module Subroutine
2
2
 
3
3
  MAJOR = 0
4
4
  MINOR = 1
5
- PATCH = 0
5
+ PATCH = 1
6
6
  PRE = nil
7
7
 
8
8
  VERSION = [MAJOR, MINOR, PATCH, PRE].compact.join('.')
@@ -93,7 +93,7 @@ module Subroutine
93
93
  assert op.perform_called
94
94
  refute op.perform_finished
95
95
 
96
- assert_equal ["Email address has gotta be @admin.com"], op.errors[:base]
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
- inherit_errors_from(u)
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.0
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-08 00:00:00.000000000 Z
11
+ date: 2015-05-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel