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