active_interaction 0.1.1

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