active_interaction 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +202 -0
- data/lib/active_interaction.rb +23 -0
- data/lib/active_interaction/base.rb +162 -0
- data/lib/active_interaction/errors.rb +6 -0
- data/lib/active_interaction/filter.rb +41 -0
- data/lib/active_interaction/filter_method.rb +17 -0
- data/lib/active_interaction/filter_methods.rb +26 -0
- data/lib/active_interaction/filters/array_filter.rb +56 -0
- data/lib/active_interaction/filters/boolean_filter.rb +30 -0
- data/lib/active_interaction/filters/date_filter.rb +31 -0
- data/lib/active_interaction/filters/date_time_filter.rb +31 -0
- data/lib/active_interaction/filters/file_filter.rb +38 -0
- data/lib/active_interaction/filters/float_filter.rb +32 -0
- data/lib/active_interaction/filters/hash_filter.rb +47 -0
- data/lib/active_interaction/filters/integer_filter.rb +31 -0
- data/lib/active_interaction/filters/model_filter.rb +40 -0
- data/lib/active_interaction/filters/string_filter.rb +25 -0
- data/lib/active_interaction/filters/time_filter.rb +44 -0
- data/lib/active_interaction/overload_hash.rb +11 -0
- data/lib/active_interaction/version.rb +3 -0
- data/spec/active_interaction/base_spec.rb +175 -0
- data/spec/active_interaction/filter_method_spec.rb +48 -0
- data/spec/active_interaction/filter_methods_spec.rb +30 -0
- data/spec/active_interaction/filter_spec.rb +29 -0
- data/spec/active_interaction/filters/array_filter_spec.rb +54 -0
- data/spec/active_interaction/filters/boolean_filter_spec.rb +40 -0
- data/spec/active_interaction/filters/date_filter_spec.rb +32 -0
- data/spec/active_interaction/filters/date_time_filter_spec.rb +32 -0
- data/spec/active_interaction/filters/file_filter_spec.rb +32 -0
- data/spec/active_interaction/filters/float_filter_spec.rb +40 -0
- data/spec/active_interaction/filters/hash_filter_spec.rb +57 -0
- data/spec/active_interaction/filters/integer_filter_spec.rb +32 -0
- data/spec/active_interaction/filters/model_filter_spec.rb +40 -0
- data/spec/active_interaction/filters/string_filter_spec.rb +16 -0
- data/spec/active_interaction/filters/time_filter_spec.rb +66 -0
- data/spec/active_interaction/integration/array_interaction_spec.rb +69 -0
- data/spec/active_interaction/integration/boolean_interaction_spec.rb +5 -0
- data/spec/active_interaction/integration/date_interaction_spec.rb +5 -0
- data/spec/active_interaction/integration/date_time_interaction_spec.rb +5 -0
- data/spec/active_interaction/integration/file_interaction_spec.rb +5 -0
- data/spec/active_interaction/integration/float_interaction_spec.rb +5 -0
- data/spec/active_interaction/integration/hash_interaction_spec.rb +69 -0
- data/spec/active_interaction/integration/integer_interaction_spec.rb +5 -0
- data/spec/active_interaction/integration/model_interaction_spec.rb +5 -0
- data/spec/active_interaction/integration/string_interaction_spec.rb +5 -0
- data/spec/active_interaction/integration/time_interaction_spec.rb +5 -0
- data/spec/active_interaction/overload_hash_spec.rb +41 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/support/filters.rb +37 -0
- data/spec/support/interactions.rb +79 -0
- metadata +278 -0
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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,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
|