active_interaction 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.
Files changed (53) hide show
  1. data/CHANGELOG.md +7 -0
  2. data/LICENSE.txt +22 -0
  3. data/README.md +202 -0
  4. data/lib/active_interaction.rb +23 -0
  5. data/lib/active_interaction/base.rb +162 -0
  6. data/lib/active_interaction/errors.rb +6 -0
  7. data/lib/active_interaction/filter.rb +41 -0
  8. data/lib/active_interaction/filter_method.rb +17 -0
  9. data/lib/active_interaction/filter_methods.rb +26 -0
  10. data/lib/active_interaction/filters/array_filter.rb +56 -0
  11. data/lib/active_interaction/filters/boolean_filter.rb +30 -0
  12. data/lib/active_interaction/filters/date_filter.rb +31 -0
  13. data/lib/active_interaction/filters/date_time_filter.rb +31 -0
  14. data/lib/active_interaction/filters/file_filter.rb +38 -0
  15. data/lib/active_interaction/filters/float_filter.rb +32 -0
  16. data/lib/active_interaction/filters/hash_filter.rb +47 -0
  17. data/lib/active_interaction/filters/integer_filter.rb +31 -0
  18. data/lib/active_interaction/filters/model_filter.rb +40 -0
  19. data/lib/active_interaction/filters/string_filter.rb +25 -0
  20. data/lib/active_interaction/filters/time_filter.rb +44 -0
  21. data/lib/active_interaction/overload_hash.rb +11 -0
  22. data/lib/active_interaction/version.rb +3 -0
  23. data/spec/active_interaction/base_spec.rb +175 -0
  24. data/spec/active_interaction/filter_method_spec.rb +48 -0
  25. data/spec/active_interaction/filter_methods_spec.rb +30 -0
  26. data/spec/active_interaction/filter_spec.rb +29 -0
  27. data/spec/active_interaction/filters/array_filter_spec.rb +54 -0
  28. data/spec/active_interaction/filters/boolean_filter_spec.rb +40 -0
  29. data/spec/active_interaction/filters/date_filter_spec.rb +32 -0
  30. data/spec/active_interaction/filters/date_time_filter_spec.rb +32 -0
  31. data/spec/active_interaction/filters/file_filter_spec.rb +32 -0
  32. data/spec/active_interaction/filters/float_filter_spec.rb +40 -0
  33. data/spec/active_interaction/filters/hash_filter_spec.rb +57 -0
  34. data/spec/active_interaction/filters/integer_filter_spec.rb +32 -0
  35. data/spec/active_interaction/filters/model_filter_spec.rb +40 -0
  36. data/spec/active_interaction/filters/string_filter_spec.rb +16 -0
  37. data/spec/active_interaction/filters/time_filter_spec.rb +66 -0
  38. data/spec/active_interaction/integration/array_interaction_spec.rb +69 -0
  39. data/spec/active_interaction/integration/boolean_interaction_spec.rb +5 -0
  40. data/spec/active_interaction/integration/date_interaction_spec.rb +5 -0
  41. data/spec/active_interaction/integration/date_time_interaction_spec.rb +5 -0
  42. data/spec/active_interaction/integration/file_interaction_spec.rb +5 -0
  43. data/spec/active_interaction/integration/float_interaction_spec.rb +5 -0
  44. data/spec/active_interaction/integration/hash_interaction_spec.rb +69 -0
  45. data/spec/active_interaction/integration/integer_interaction_spec.rb +5 -0
  46. data/spec/active_interaction/integration/model_interaction_spec.rb +5 -0
  47. data/spec/active_interaction/integration/string_interaction_spec.rb +5 -0
  48. data/spec/active_interaction/integration/time_interaction_spec.rb +5 -0
  49. data/spec/active_interaction/overload_hash_spec.rb +41 -0
  50. data/spec/spec_helper.rb +6 -0
  51. data/spec/support/filters.rb +37 -0
  52. data/spec/support/interactions.rb +79 -0
  53. metadata +278 -0
@@ -0,0 +1,7 @@
1
+ # 0.1.1
2
+
3
+ - Correct gemspec dependencies on activemodel.
4
+
5
+ # 0.1.0
6
+
7
+ - Initial release.
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Aaron Lasseigne
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,202 @@
1
+ # ActiveInteraction
2
+
3
+ [![Gem Version][]](https://badge.fury.io/rb/active_interaction)
4
+ [![Build Status][]](https://travis-ci.org/orgsync/active_interaction)
5
+ [![Coverage Status][]](https://coveralls.io/r/orgsync/active_interaction)
6
+ [![Code Climate][]](https://codeclimate.com/github/orgsync/active_interaction)
7
+ [![Dependency Status][]](https://gemnasium.com/orgsync/active_interaction)
8
+
9
+ At first it seemed alright. A little business logic in a controller or model
10
+ wasn't going to hurt anything. Then one day you wake up and you're surrounded
11
+ by fat models and unweildy controllers. Curled up and crying in the
12
+ corner, you can't help but wonder how it came to this.
13
+
14
+ Take back control. Slim down models and wrangle monstrous controller methods
15
+ with ActiveInteraction.
16
+
17
+ ## Installation
18
+
19
+ This project uses [semantic versioning][].
20
+
21
+ Add it to your Gemfile:
22
+
23
+ ~~~ rb
24
+ gem 'active_interaction', '~> 0.1.1'
25
+ ~~~
26
+
27
+ And then execute:
28
+
29
+ ~~~ sh
30
+ $ bundle
31
+ ~~~
32
+
33
+ Or install it yourself as:
34
+
35
+ ~~~ rb
36
+ $ gem install active_interaction
37
+ ~~~
38
+
39
+ ## What do I get?
40
+
41
+ ActiveInteraction::Base lets you create interaction models. These models ensure
42
+ that certain options are provided and that those options are in the format you
43
+ want them in. If the options are valid it will call `execute`, store the return
44
+ value of that method in `result`, and return an instance of your ActiveInteraction::Base
45
+ subclass. Let's looks at a simple example:
46
+
47
+ ~~~ rb
48
+ # Define an interaction that signs up a user.
49
+ class UserSignup < ActiveInteraction::Base
50
+ # required
51
+ string :email, :name
52
+
53
+ # optional
54
+ boolean :newsletter_subscribe, allow_nil: true
55
+
56
+ # ActiveRecord validations
57
+ validates :email, format: EMAIL_REGEX
58
+
59
+ # The execute method is called only if the options validate. It does your
60
+ # business action. The return value will be stored in `result`.
61
+ def execute
62
+ user = User.create!(email: email, name: name)
63
+ NewsletterSubscriptions.create(email: email, user_id: user.id) if newsletter_subscribe
64
+ UserMailer.async(:deliver_welcome, user.id)
65
+ user
66
+ end
67
+ end
68
+
69
+ # In a controller action (for instance), you can run it:
70
+ def new
71
+ @signup = UserSignup.new
72
+ end
73
+
74
+ def create
75
+ @signup = UserSignup.run(params[:user])
76
+
77
+ # Then check to see if it worked:
78
+ if @signup.valid?
79
+ redirect_to welcome_path(user_id: signup.result.id)
80
+ else
81
+ render action: :new
82
+ end
83
+ end
84
+ ~~~
85
+
86
+ You may have noticed that ActiveInteraction::Base quacks like ActiveRecord::Base.
87
+ It can use validations from your Rails application and check option validity with
88
+ `valid?`. Any errors are added to `errors` which works exactly like an ActiveRecord
89
+ model.
90
+
91
+ ## How do I call an interaction?
92
+
93
+ There are two way to call an interaction. Given UserSignup, you can do this:
94
+
95
+ ~~~ rb
96
+ outcome = UserSignup.run(params)
97
+ if outcome.valid?
98
+ # Do something with outcome.result...
99
+ else
100
+ # Do something with outcome.errors...
101
+ end
102
+ ~~~
103
+
104
+ Or, you can do this:
105
+
106
+ ~~~ rb
107
+ result = UserSignup.run!(params)
108
+ # Either returns the result of execute,
109
+ # or raises ActiveInteraction::InteractionInvalid
110
+ ~~~
111
+
112
+ ## What can I pass to an interaction?
113
+
114
+ Interactions only accept a Hash for `run` and `run!`.
115
+
116
+ ~~~ rb
117
+ # A user comments on an article
118
+ class CreateComment < ActiveInteraction::Base
119
+ model :article, :user
120
+ string :comment
121
+
122
+ validates :comment, length: {maximum: 500}
123
+
124
+ def execute; ...; end
125
+ end
126
+
127
+ def somewhere
128
+ outcome = CreateComment.run(
129
+ comment: params[:comment],
130
+ article: Article.find(params[:article_id]),
131
+ user: current_user
132
+ )
133
+ end
134
+ ~~~
135
+
136
+ ## How do I define an interaction?
137
+
138
+ 1. Subclass ActiveInteraction::Base
139
+
140
+ ~~~ rb
141
+ class YourInteraction < ActiveInteraction::Base
142
+ # ...
143
+ end
144
+ ~~~
145
+
146
+ 2. Define your attributes:
147
+
148
+ ~~~ rb
149
+ string :name, :state
150
+ integer :age
151
+ boolean :is_special
152
+ model :account
153
+ array :tags, allow_nil: true do
154
+ string
155
+ end
156
+ hash :prefs, allow_nil: true do
157
+ boolean :smoking
158
+ boolean :view
159
+ end
160
+ date :arrives_on, default: Date.today
161
+ date :departs_on, default: Date.tomorrow
162
+ ~~~
163
+
164
+ 3. Use any additional validations you need:
165
+
166
+ ~~~ rb
167
+ validates :name, length: {maximum: 10}
168
+ validates :state, inclusion: {in: %w(AL AK AR ... WY)}
169
+ validate arrives_before_departs
170
+
171
+ private
172
+
173
+ def arrive_before_departs
174
+ if departs_on <= arrives_on
175
+ errors.add(:departs_on, 'must come after the arrival time')
176
+ end
177
+ end
178
+ ~~~
179
+
180
+ 4. Define your execute method. It can return whatever you like:
181
+
182
+ ~~~ rb
183
+ def execute
184
+ record = do_thing(...)
185
+ # ...
186
+ record
187
+ end
188
+ ~~~
189
+
190
+ A full list of methods can be found [here](http://www.rubydoc.info/github/orgsync/active_interaction/master/ActiveInteraction/Base).
191
+
192
+ ## Credits
193
+
194
+ This project was inspired by the fantastic work done in [Mutations][].
195
+
196
+ [build status]: https://travis-ci.org/orgsync/active_interaction.png
197
+ [code climate]: https://codeclimate.com/github/orgsync/active_interaction.png
198
+ [coverage status]: https://coveralls.io/repos/orgsync/active_interaction/badge.png
199
+ [dependency status]: https://gemnasium.com/orgsync/active_interaction.png
200
+ [gem version]: https://badge.fury.io/rb/active_interaction.png
201
+ [mutations]: https://github.com/cypriss/mutations
202
+ [semantic versioning]: http://semver.org
@@ -0,0 +1,23 @@
1
+ require 'active_model'
2
+
3
+ require 'active_interaction/version'
4
+ require 'active_interaction/errors'
5
+ require 'active_interaction/overload_hash'
6
+ require 'active_interaction/filter'
7
+ require 'active_interaction/filter_method'
8
+ require 'active_interaction/filter_methods'
9
+ require 'active_interaction/filters/array_filter'
10
+ require 'active_interaction/filters/boolean_filter'
11
+ require 'active_interaction/filters/date_filter'
12
+ require 'active_interaction/filters/date_time_filter'
13
+ require 'active_interaction/filters/file_filter'
14
+ require 'active_interaction/filters/float_filter'
15
+ require 'active_interaction/filters/hash_filter'
16
+ require 'active_interaction/filters/integer_filter'
17
+ require 'active_interaction/filters/model_filter'
18
+ require 'active_interaction/filters/string_filter'
19
+ require 'active_interaction/filters/time_filter'
20
+ require 'active_interaction/base'
21
+
22
+ # @since 0.1.0
23
+ module ActiveInteraction; end
@@ -0,0 +1,162 @@
1
+ require 'active_support/core_ext/hash/indifferent_access'
2
+
3
+ module ActiveInteraction
4
+ # @abstract Subclass and override {#execute} to implement
5
+ # a custom ActiveInteraction class.
6
+ #
7
+ # @example
8
+ # class ExampleInteraction < ActiveInteraction::Base
9
+ # # Required
10
+ # integer :a, :b
11
+ #
12
+ # # Optional
13
+ # integer :c, allow_nil: true
14
+ #
15
+ # def execute
16
+ # sum = a + b
17
+ # c.nil? ? sum : sum + c
18
+ # end
19
+ # end
20
+ #
21
+ # outcome = ExampleInteraction.run(a: 1, b: 2, c: 3)
22
+ # if outcome.valid?
23
+ # p outcome.result
24
+ # else
25
+ # p outcome.errors
26
+ # end
27
+ class Base
28
+ extend ::ActiveModel::Naming
29
+ include ::ActiveModel::Conversion
30
+ include ::ActiveModel::Validations
31
+
32
+ # @private
33
+ def new_record?
34
+ true
35
+ end
36
+
37
+ # @private
38
+ def persisted?
39
+ false
40
+ end
41
+
42
+ extend OverloadHash
43
+
44
+ # Returns the output from {#execute} if there are no validation errors or
45
+ # `nil` otherwise.
46
+ #
47
+ # @return [Nil] if there are validation errors.
48
+ # @return [Object] if there are no validation errors.
49
+ attr_reader :result
50
+
51
+ # @private
52
+ def initialize(options = {})
53
+ options = options.with_indifferent_access
54
+
55
+ if options.has_key?(:result)
56
+ raise ArgumentError, ':result is reserved and can not be used'
57
+ end
58
+
59
+ options.each do |attribute, value|
60
+ if respond_to?("#{attribute}=")
61
+ send("#{attribute}=", value)
62
+ else
63
+ instance_variable_set("@#{attribute}", value)
64
+ end
65
+ end
66
+ end
67
+
68
+ # Runs the business logic associated with the interactor. The method is only
69
+ # run when there are no validation errors. The return value is placed into
70
+ # {#result}. This method must be overridden in the subclass.
71
+ #
72
+ # @raise [NotImplementedError] if the method is not defined.
73
+ def execute
74
+ raise NotImplementedError
75
+ end
76
+
77
+ # @!macro [new] run_attributes
78
+ # @param options [Hash] Attribute values to set.
79
+
80
+ # Runs validations and if there are no errors it will call {#execute}.
81
+ #
82
+ # @macro run_attributes
83
+ #
84
+ # @return [ActiveInteraction::Base] An instance of the class `run` is called on.
85
+ def self.run(options = {})
86
+ me = new(options)
87
+
88
+ me.instance_variable_set(:@result, me.execute) if me.valid?
89
+
90
+ me
91
+ end
92
+
93
+ # Like {.run} except that it returns the value of {#execute} or raises an
94
+ # exception if there were any validation errors.
95
+ #
96
+ # @macro run_attributes
97
+ #
98
+ # @raise [InteractionInvalid] if there are any errors on the model.
99
+ #
100
+ # @return The return value of {#execute}.
101
+ def self.run!(options = {})
102
+ outcome = run(options)
103
+ raise InteractionInvalid if outcome.invalid?
104
+ outcome.result
105
+ end
106
+
107
+ # @private
108
+ def self.method_missing(filter_type, *args, &block)
109
+ klass = Filter.factory(filter_type)
110
+ options = args.last.is_a?(Hash) ? args.pop : {}
111
+ args.each do |attribute|
112
+ filter_attr_accessor(klass, attribute, options, &block)
113
+
114
+ filter_validator(attribute, filter_type, klass, options, &block)
115
+ end
116
+ end
117
+ private_class_method :method_missing
118
+
119
+ # @private
120
+ def self.filter_attr_accessor(filter, attribute, options, &block)
121
+ attr_writer attribute
122
+
123
+ if options.has_key?(:default)
124
+ begin
125
+ default_value = filter.prepare(attribute, options.delete(:default), options, &block)
126
+ rescue InvalidValue
127
+ raise InvalidDefaultValue
128
+ end
129
+ end
130
+
131
+ define_method(attribute) do
132
+ instance_variable_name = "@#{attribute}"
133
+ if instance_variable_defined?(instance_variable_name)
134
+ instance_variable_get(instance_variable_name)
135
+ else
136
+ default_value
137
+ end
138
+ end
139
+ end
140
+ private_class_method :filter_attr_accessor
141
+
142
+ # @private
143
+ def self.filter_validator(attribute, type, filter, options, &block)
144
+ validator = "_validate__#{attribute}__#{type}"
145
+
146
+ validate validator
147
+
148
+ define_method(validator) do
149
+ begin
150
+ filter.prepare(attribute, send(attribute), options, &block)
151
+ rescue MissingValue
152
+ errors.add(attribute, 'is required')
153
+ rescue InvalidValue
154
+ errors.add(attribute,
155
+ "is not a valid #{type.to_s.humanize.downcase}")
156
+ end
157
+ end
158
+ private validator
159
+ end
160
+ private_class_method :filter_validator
161
+ end
162
+ end
@@ -0,0 +1,6 @@
1
+ module ActiveInteraction
2
+ class InteractionInvalid < ::StandardError; end
3
+ class InvalidDefaultValue < ::StandardError; end
4
+ class InvalidValue < ::StandardError; end
5
+ class MissingValue < ::StandardError; end
6
+ end
@@ -0,0 +1,41 @@
1
+ module ActiveInteraction
2
+ # @!macro [new] attribute_method_params
3
+ # @param *attributes [Symbol] One or more attributes to create.
4
+ # @param options [Hash]
5
+ # @option options [Boolean] :allow_nil Allow a `nil` value.
6
+ # @option options [Object] :default Value to use if `nil` is given.
7
+
8
+ # @private
9
+ class Filter
10
+ def self.factory(type)
11
+ klass = "#{type.to_s.camelize}Filter"
12
+
13
+ raise NoMethodError unless ActiveInteraction.const_defined?(klass)
14
+
15
+ ActiveInteraction.const_get(klass)
16
+ end
17
+
18
+ def self.prepare(key, value, options = {}, &block)
19
+ case value
20
+ when NilClass
21
+ return_nil(options[:allow_nil])
22
+ else
23
+ bad_value
24
+ end
25
+ end
26
+
27
+ def self.return_nil(allow_nil)
28
+ if allow_nil
29
+ nil
30
+ else
31
+ raise MissingValue
32
+ end
33
+ end
34
+ private_class_method :return_nil
35
+
36
+ def self.bad_value
37
+ raise InvalidValue
38
+ end
39
+ private_class_method :bad_value
40
+ end
41
+ end