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