interactor-schema 0.2.0
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 +7 -0
- data/.gitignore +9 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +175 -0
- data/Rakefile +6 -0
- data/bin/console +10 -0
- data/bin/setup +8 -0
- data/interactor-schema.gemspec +27 -0
- data/lib/interactor/schema.rb +37 -0
- data/lib/interactor/schema/context.rb +80 -0
- data/lib/interactor/schema/context_extension.rb +10 -0
- data/lib/interactor/schema/interactor_extension.rb +34 -0
- data/lib/interactor/schema/version.rb +5 -0
- data/spec/interactor/schema/context_spec.rb +210 -0
- data/spec/interactor/schema/interactor_extension_spec.rb +41 -0
- data/spec/interactor/schema_spec.rb +83 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/support/lint.rb +138 -0
- metadata +139 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1ecc8a0bdc552d23ab22db288cef8ba75f496eec
|
4
|
+
data.tar.gz: 332f9271bcde51e416b2e65fe9a1337c873d8a10
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3390e8c315799c36e88f643cfaa2b5c11bf7bcc8b775e0b727ffff69580d0ad824f064a50f8221e3b01e07da34de9d3bcc58860dcd86506852913b6876432cad
|
7
|
+
data.tar.gz: 338f3d9000de50f1b1d97ea89947ed174911c6e6d51c6ca7946317378e4df71c760b68635f66c684810ae30912995967a26716db39918d69811a950133e8f5a8
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Bernardo Farah
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
# Interactor::Schema
|
2
|
+
|
3
|
+
[](https://travis-ci.org/berfarah/interactor-schema)
|
4
|
+
|
5
|
+
Interactor::Schema provides an enforcable Interactor Context via a `schema`
|
6
|
+
class method.
|
7
|
+
|
8
|
+
This gem was born out of frustration. In using Interactor, I often wouldn't know
|
9
|
+
what methods would be defined on it. By forcing a schema upfront, and also
|
10
|
+
requiring methods in Interactor classes, we bring order to
|
11
|
+
`Interactor::Context`.
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Put this in your application's Gemfile:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'interactor'
|
19
|
+
gem 'interactor-schema'
|
20
|
+
```
|
21
|
+
|
22
|
+
And then execute:
|
23
|
+
|
24
|
+
$ bundle
|
25
|
+
|
26
|
+
Or install it yourself as:
|
27
|
+
|
28
|
+
$ gem install interactor-schema
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
|
32
|
+
See [here](https://github.com/collectiveidea/interactor) for the official
|
33
|
+
Interactor documentation.
|
34
|
+
|
35
|
+
|
36
|
+
### Setup
|
37
|
+
|
38
|
+
When you include `Interactor::Schema`, you are able to define a schema for valid
|
39
|
+
context methods using the `schema` DSL method.
|
40
|
+
|
41
|
+
```rb
|
42
|
+
class SaveUser
|
43
|
+
include Interactor::Schema
|
44
|
+
schema :name
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
### Calling methods
|
49
|
+
|
50
|
+
When the attribute is part of the valid schema, you will have no problems
|
51
|
+
accessing it as usual:
|
52
|
+
|
53
|
+
```rb
|
54
|
+
context = SaveUser.call(name: "Bob")
|
55
|
+
context.name
|
56
|
+
# => "Bob"
|
57
|
+
```
|
58
|
+
|
59
|
+
For attributes outside the schema, you will get `NoMethodError`s. The same thing
|
60
|
+
applies when trying to set an attribute outside of the schema.
|
61
|
+
|
62
|
+
```rb
|
63
|
+
context = SaveUser.call(age: 28)
|
64
|
+
context.age
|
65
|
+
# NoMethodError: undefined method 'age' for #<SaveUser:0x007f9521298b10>
|
66
|
+
```
|
67
|
+
|
68
|
+
### Use with Organizers
|
69
|
+
|
70
|
+
`Interactor::Schema` works great with `Interactor::Organizer`s!
|
71
|
+
|
72
|
+
```rb
|
73
|
+
class SaveUser
|
74
|
+
include Interactor::Schema
|
75
|
+
include Interactor::Organizer
|
76
|
+
|
77
|
+
schema :name
|
78
|
+
organize ValidateInfo, SendMailer
|
79
|
+
end
|
80
|
+
|
81
|
+
class ValidateInfo
|
82
|
+
include Interactor
|
83
|
+
|
84
|
+
def call
|
85
|
+
# These will work, since name is in our schema
|
86
|
+
context.name
|
87
|
+
context.name = "Bob"
|
88
|
+
|
89
|
+
# These will fail with NoMethodErrors since age is not in our schema
|
90
|
+
context.age
|
91
|
+
context.age = 28
|
92
|
+
end
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
The initial schema will be propagated through the entire chain, meaning no
|
97
|
+
attributes outside of `name` can be set. This is particularly helpful for
|
98
|
+
maintaining clarity of what information is being consumed by a chain of
|
99
|
+
`Interactor`s.
|
100
|
+
|
101
|
+
### Multiple Schemas
|
102
|
+
|
103
|
+
In the case of multiple schemas being provided only the first one will be used
|
104
|
+
(others will be discarded). The idea is that you should know upfront what
|
105
|
+
attributes will be needed.
|
106
|
+
|
107
|
+
Modifying the schema in the middle of the chain could result in confusing
|
108
|
+
behavior.
|
109
|
+
|
110
|
+
|
111
|
+
### Interactor Helper Methods
|
112
|
+
|
113
|
+
This gem comes with two helper methods - one that should help enforce context
|
114
|
+
cleanliness and one to delegate to the context.
|
115
|
+
|
116
|
+
#### require_in_context
|
117
|
+
|
118
|
+
`Interactor::require_in_context` ensures that the attributes provided as
|
119
|
+
arguments are present and not `nil?` when the service is called.
|
120
|
+
|
121
|
+
```rb
|
122
|
+
class SaveUser
|
123
|
+
include Interactor
|
124
|
+
require_in_context :name
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
This will fail:
|
129
|
+
|
130
|
+
```rb
|
131
|
+
SaveUser.call
|
132
|
+
# => ArgumentError: Missing the following attributes in context: name
|
133
|
+
```
|
134
|
+
|
135
|
+
Whereas this will work:
|
136
|
+
|
137
|
+
```rb
|
138
|
+
SaveUser.call(name: "Bob")
|
139
|
+
# => #<Interactor::Context:0x007f9521298b10>
|
140
|
+
```
|
141
|
+
|
142
|
+
#### delegate_to_context
|
143
|
+
|
144
|
+
`Interactor::delegate_to_context` creates a method that delegates to the
|
145
|
+
context.
|
146
|
+
|
147
|
+
```rb
|
148
|
+
class SaveUser
|
149
|
+
include Interactor
|
150
|
+
|
151
|
+
delegate_to_context :name
|
152
|
+
|
153
|
+
# is equivalent to:
|
154
|
+
|
155
|
+
def name
|
156
|
+
context.name
|
157
|
+
end
|
158
|
+
end
|
159
|
+
```
|
160
|
+
|
161
|
+
## Development
|
162
|
+
|
163
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
164
|
+
|
165
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
166
|
+
|
167
|
+
## Contributing
|
168
|
+
|
169
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/berfarah/interactor-schema.
|
170
|
+
|
171
|
+
|
172
|
+
## License
|
173
|
+
|
174
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
175
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "interactor/schema"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
require "pry"
|
10
|
+
Pry.start
|
data/bin/setup
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'interactor/schema/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "interactor-schema"
|
8
|
+
spec.version = Interactor::Schema::VERSION
|
9
|
+
spec.authors = ["Bernardo Farah"]
|
10
|
+
spec.email = ["ber@bernardo.me"]
|
11
|
+
|
12
|
+
spec.summary = "Enforce a schema with for Interactor Context"
|
13
|
+
spec.description = "Interactor::Schema provides an enforcable Interactor Context"
|
14
|
+
spec.homepage = "https://github.com/berfarah/interactor-schema"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files`.split($/)
|
18
|
+
spec.test_files = spec.files.grep(/^spec/)
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "interactor", "~> 3.1"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.13"
|
24
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
25
|
+
spec.add_development_dependency "rspec", "~> 3.1"
|
26
|
+
spec.add_development_dependency "pry", "~> 0.10"
|
27
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "interactor"
|
2
|
+
require "interactor/schema/context"
|
3
|
+
require "interactor/schema/context_extension"
|
4
|
+
require "interactor/schema/interactor_extension"
|
5
|
+
require "interactor/schema/version"
|
6
|
+
|
7
|
+
module Interactor
|
8
|
+
module Schema
|
9
|
+
# Internal: Install Interactor::Schema's behavior in the given class.
|
10
|
+
def self.included(base)
|
11
|
+
base.class_eval do
|
12
|
+
# Don't override methods when an Interactor has already been included
|
13
|
+
# For example, when Organizer has already been included
|
14
|
+
include Interactor unless base.ancestors.include? Interactor
|
15
|
+
|
16
|
+
extend ClassMethods
|
17
|
+
include InstanceMethods
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
def schema(*method_names)
|
23
|
+
@schema = method_names.flatten
|
24
|
+
end
|
25
|
+
|
26
|
+
def _schema
|
27
|
+
@schema ||= []
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module InstanceMethods
|
32
|
+
def initialize(context = {})
|
33
|
+
@context = Interactor::Schema::Context.build(context, self.class._schema)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Interactor
|
2
|
+
module Schema
|
3
|
+
class Context
|
4
|
+
def self.build(context = {}, schema = [])
|
5
|
+
self === context ? context : new(schema).tap do |instance|
|
6
|
+
instance.assign(context)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(schema = [])
|
11
|
+
@schema = schema
|
12
|
+
@table = {}
|
13
|
+
define_schema_methods(schema)
|
14
|
+
end
|
15
|
+
|
16
|
+
def [](key)
|
17
|
+
@table[key]
|
18
|
+
end
|
19
|
+
|
20
|
+
def assign(context)
|
21
|
+
filtered_context = context.select { |k, _| _schema.include?(k) }
|
22
|
+
@table.merge!(filtered_context)
|
23
|
+
end
|
24
|
+
|
25
|
+
def success?
|
26
|
+
!failure?
|
27
|
+
end
|
28
|
+
|
29
|
+
def failure?
|
30
|
+
@failure || false
|
31
|
+
end
|
32
|
+
|
33
|
+
def fail!(context = {})
|
34
|
+
@table.merge!(context)
|
35
|
+
@failure = true
|
36
|
+
raise Interactor::Failure, self
|
37
|
+
end
|
38
|
+
|
39
|
+
def called!(interactor)
|
40
|
+
_called << interactor
|
41
|
+
end
|
42
|
+
|
43
|
+
def rollback!
|
44
|
+
return false if @rolled_back
|
45
|
+
_called.reverse_each(&:rollback)
|
46
|
+
@rolled_back = true
|
47
|
+
end
|
48
|
+
|
49
|
+
def _called
|
50
|
+
@called ||= []
|
51
|
+
end
|
52
|
+
|
53
|
+
def _schema
|
54
|
+
@schema
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_h
|
58
|
+
@table
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
attr_reader :table
|
64
|
+
|
65
|
+
def define_schema_methods(schema)
|
66
|
+
singleton_class.class_exec(schema) do |names|
|
67
|
+
names.each do |name|
|
68
|
+
define_method(name) do
|
69
|
+
@table[name]
|
70
|
+
end
|
71
|
+
|
72
|
+
define_method("#{name}=") do |value|
|
73
|
+
@table[name] = value
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Interactor
|
2
|
+
class Context
|
3
|
+
# Overriding build method to continue with a Schema Context
|
4
|
+
# when one is found
|
5
|
+
def self.build(context = {})
|
6
|
+
return context if context.is_a?(Interactor::Schema::Context)
|
7
|
+
self === context ? context : new(context)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Interactor
|
2
|
+
module ClassMethods
|
3
|
+
# Depends on Interactor::Hook
|
4
|
+
def require_in_context(*attributes)
|
5
|
+
before :require_in_context!
|
6
|
+
@required_in_context = attributes
|
7
|
+
end
|
8
|
+
|
9
|
+
def delegate_to_context(*attributes)
|
10
|
+
attributes.each do |attribute|
|
11
|
+
define_method(attribute) { context[attribute] }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :required_in_context
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def required_in_context
|
21
|
+
self.class.required_in_context
|
22
|
+
end
|
23
|
+
|
24
|
+
def require_in_context!
|
25
|
+
missing_attributes = required_in_context.select do |attribute|
|
26
|
+
context[attribute].nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
return if missing_attributes.empty?
|
30
|
+
raise ArgumentError, <<-MESSAGE.strip
|
31
|
+
Missing the following attributes in context: #{missing_attributes.join(', ')}
|
32
|
+
MESSAGE
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
module Interactor::Schema
|
2
|
+
describe Context do
|
3
|
+
describe ".build" do
|
4
|
+
it "doesn't respond to methods when no schema is given" do
|
5
|
+
context = Context.build(foo: "bar")
|
6
|
+
|
7
|
+
expect(context).to be_a(Context)
|
8
|
+
expect { context.foo }.to raise_error(NoMethodError)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "responds to methods when a schema is given" do
|
12
|
+
context = Context.build({ foo: "bar" }, [:foo])
|
13
|
+
|
14
|
+
expect(context).to be_a(Context)
|
15
|
+
expect(context.foo).to eq("bar")
|
16
|
+
end
|
17
|
+
|
18
|
+
it "only responds to methods defined in the schema" do
|
19
|
+
context = Context.build({ foo: "bar" }, [:baz])
|
20
|
+
|
21
|
+
expect(context).to be_a(Context)
|
22
|
+
expect { context.foo }.to raise_error(NoMethodError)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "#_schema" do
|
27
|
+
let(:context) { Context.build({}, schema) }
|
28
|
+
let(:schema) { [:foo] }
|
29
|
+
|
30
|
+
it "is empty by default" do
|
31
|
+
context = Context.build
|
32
|
+
expect(context._schema).to eq([])
|
33
|
+
end
|
34
|
+
|
35
|
+
it "holds onto the schema" do
|
36
|
+
expect(context._schema).to eq(schema)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#to_h" do
|
41
|
+
let(:context) { Context.build({ foo: "one", bar: "two" }, [:foo]) }
|
42
|
+
it "only records allowed values" do
|
43
|
+
expect(context.to_h).to eq(foo: "one")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# NOTE:
|
48
|
+
# The tests below are copied from the original Interactor::Context
|
49
|
+
# The only thing modified was to add the context argument where needed.
|
50
|
+
#
|
51
|
+
# You can find them here:
|
52
|
+
# https://github.com/collectiveidea/interactor/blob/master/spec/interactor/context_spec.rb
|
53
|
+
describe ".build" do
|
54
|
+
it "builds an empty context if no hash is given" do
|
55
|
+
context = Context.build
|
56
|
+
|
57
|
+
expect(context).to be_a(Context)
|
58
|
+
expect(context.send(:table)).to eq({})
|
59
|
+
end
|
60
|
+
|
61
|
+
it "doesn't affect the original hash" do
|
62
|
+
hash = { foo: "bar" }
|
63
|
+
context = Context.build(hash, [:foo])
|
64
|
+
|
65
|
+
expect(context).to be_a(Context)
|
66
|
+
expect {
|
67
|
+
context.foo = "baz"
|
68
|
+
}.not_to change {
|
69
|
+
hash[:foo]
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
it "preserves an already built context" do
|
74
|
+
context1 = Context.build({ foo: "bar" }, [:foo])
|
75
|
+
context2 = Context.build(context1)
|
76
|
+
|
77
|
+
expect(context2).to be_a(Context)
|
78
|
+
expect {
|
79
|
+
context2.foo = "baz"
|
80
|
+
}.to change {
|
81
|
+
context1.foo
|
82
|
+
}.from("bar").to("baz")
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe "#success?" do
|
87
|
+
let(:context) { Context.build }
|
88
|
+
|
89
|
+
it "is true by default" do
|
90
|
+
expect(context.success?).to eq(true)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe "#failure?" do
|
95
|
+
let(:context) { Context.build }
|
96
|
+
|
97
|
+
it "is false by default" do
|
98
|
+
expect(context.failure?).to eq(false)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe "#fail!" do
|
103
|
+
let(:context) { Context.build({ foo: "bar" }, [:foo]) }
|
104
|
+
|
105
|
+
it "sets success to false" do
|
106
|
+
expect {
|
107
|
+
context.fail! rescue nil
|
108
|
+
}.to change {
|
109
|
+
context.success?
|
110
|
+
}.from(true).to(false)
|
111
|
+
end
|
112
|
+
|
113
|
+
it "sets failure to true" do
|
114
|
+
expect {
|
115
|
+
context.fail! rescue nil
|
116
|
+
}.to change {
|
117
|
+
context.failure?
|
118
|
+
}.from(false).to(true)
|
119
|
+
end
|
120
|
+
|
121
|
+
it "preserves failure" do
|
122
|
+
context.fail! rescue nil
|
123
|
+
|
124
|
+
expect {
|
125
|
+
context.fail! rescue nil
|
126
|
+
}.not_to change {
|
127
|
+
context.failure?
|
128
|
+
}
|
129
|
+
end
|
130
|
+
|
131
|
+
it "preserves the context" do
|
132
|
+
expect {
|
133
|
+
context.fail! rescue nil
|
134
|
+
}.not_to change {
|
135
|
+
context.foo
|
136
|
+
}
|
137
|
+
end
|
138
|
+
|
139
|
+
it "updates the context" do
|
140
|
+
expect {
|
141
|
+
context.fail!(foo: "baz") rescue nil
|
142
|
+
}.to change {
|
143
|
+
context.foo
|
144
|
+
}.from("bar").to("baz")
|
145
|
+
end
|
146
|
+
|
147
|
+
it "raises failure" do
|
148
|
+
expect {
|
149
|
+
context.fail!
|
150
|
+
}.to raise_error(Interactor::Failure)
|
151
|
+
end
|
152
|
+
|
153
|
+
it "makes the context available from the failure" do
|
154
|
+
begin
|
155
|
+
context.fail!
|
156
|
+
rescue Interactor::Failure => error
|
157
|
+
expect(error.context).to eq(context)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
describe "#called!" do
|
163
|
+
let(:context) { Context.build }
|
164
|
+
let(:instance1) { double(:instance1) }
|
165
|
+
let(:instance2) { double(:instance2) }
|
166
|
+
|
167
|
+
it "appends to the internal list of called instances" do
|
168
|
+
expect {
|
169
|
+
context.called!(instance1)
|
170
|
+
context.called!(instance2)
|
171
|
+
}.to change {
|
172
|
+
context._called
|
173
|
+
}.from([]).to([instance1, instance2])
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
describe "#rollback!" do
|
178
|
+
let(:context) { Context.build }
|
179
|
+
let(:instance1) { double(:instance1) }
|
180
|
+
let(:instance2) { double(:instance2) }
|
181
|
+
|
182
|
+
before do
|
183
|
+
allow(context).to receive(:_called) { [instance1, instance2] }
|
184
|
+
end
|
185
|
+
|
186
|
+
it "rolls back each instance in reverse order" do
|
187
|
+
expect(instance2).to receive(:rollback).once.with(no_args).ordered
|
188
|
+
expect(instance1).to receive(:rollback).once.with(no_args).ordered
|
189
|
+
|
190
|
+
context.rollback!
|
191
|
+
end
|
192
|
+
|
193
|
+
it "ignores subsequent attempts" do
|
194
|
+
expect(instance2).to receive(:rollback).once
|
195
|
+
expect(instance1).to receive(:rollback).once
|
196
|
+
|
197
|
+
context.rollback!
|
198
|
+
context.rollback!
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
describe "#_called" do
|
203
|
+
let(:context) { Context.build }
|
204
|
+
|
205
|
+
it "is empty by default" do
|
206
|
+
expect(context._called).to eq([])
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
describe Interactor do
|
2
|
+
describe ".require_in_context" do
|
3
|
+
let(:interactor) do
|
4
|
+
Class.new do
|
5
|
+
include Interactor
|
6
|
+
require_in_context :foo
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
it "fails if the argument isn't passed in" do
|
11
|
+
expect { interactor.call }.to raise_error(ArgumentError)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "fails if the argument is is nil" do
|
15
|
+
expect { interactor.call(foo: nil) }.to raise_error(ArgumentError)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "succeeds if the argument is passed in" do
|
19
|
+
expect { interactor.call(foo: "bar") }.not_to raise_error
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe ".delegate_to_context" do
|
24
|
+
let(:interactor) do
|
25
|
+
Class.new do
|
26
|
+
include Interactor
|
27
|
+
delegate_to_context :foo
|
28
|
+
|
29
|
+
def call
|
30
|
+
foo
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
let(:context) { double("Context") }
|
35
|
+
|
36
|
+
it "delegates to the context" do
|
37
|
+
expect_any_instance_of(Interactor::Context).to receive(:[]).with(:foo).once
|
38
|
+
interactor.call
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Interactor
|
2
|
+
describe Schema do
|
3
|
+
include_examples :lint
|
4
|
+
|
5
|
+
describe ".schema" do
|
6
|
+
context "with an Interactor" do
|
7
|
+
let(:interactor) do
|
8
|
+
Class.new do
|
9
|
+
include Interactor
|
10
|
+
include Interactor::Schema
|
11
|
+
|
12
|
+
schema :foo
|
13
|
+
end
|
14
|
+
end
|
15
|
+
let(:context) { double(:context) }
|
16
|
+
|
17
|
+
it "s schema is passed to its context" do
|
18
|
+
expect(Interactor::Schema::Context).to receive(:build).once.with({ foo: "bar" }, [:foo]) { context }
|
19
|
+
|
20
|
+
instance = interactor.new(foo: "bar")
|
21
|
+
|
22
|
+
expect(instance).to be_a(interactor)
|
23
|
+
expect(instance.context).to eq(context)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context "with an Interactor::Organizer" do
|
28
|
+
let(:interactor1) do
|
29
|
+
Class.new do
|
30
|
+
include Interactor
|
31
|
+
|
32
|
+
def call
|
33
|
+
context.baz = "yolo"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
let(:interactor2) do
|
39
|
+
Class.new.send(:include, Interactor)
|
40
|
+
end
|
41
|
+
|
42
|
+
let(:organizer) do
|
43
|
+
# Need to cache these for some reason
|
44
|
+
klass_1 = interactor1
|
45
|
+
klass_2 = interactor2
|
46
|
+
|
47
|
+
Class.new do
|
48
|
+
include Interactor::Organizer
|
49
|
+
include Interactor::Schema
|
50
|
+
|
51
|
+
schema :foo, :baz
|
52
|
+
organize klass_1, klass_2
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
it "passes around its context" do
|
57
|
+
context = organizer.call(foo: "bar")
|
58
|
+
|
59
|
+
expect(context.foo).to eq("bar")
|
60
|
+
expect(context.baz).to eq("yolo")
|
61
|
+
end
|
62
|
+
|
63
|
+
context "when assigning an attribute not available in the schema" do
|
64
|
+
let(:interactor2) do
|
65
|
+
Class.new do
|
66
|
+
include Interactor
|
67
|
+
|
68
|
+
def call
|
69
|
+
context.undefined = "hi"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
it "raises a NoMethodError" do
|
75
|
+
expect {
|
76
|
+
organizer.call(foo: "bar")
|
77
|
+
}.to raise_error(NoMethodError)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
# NOTE:
|
2
|
+
# Copied directly from the official interactor repo:
|
3
|
+
# https://github.com/collectiveidea/interactor/blob/master/spec/support/lint.rb
|
4
|
+
shared_examples :lint do
|
5
|
+
let(:interactor) { Class.new.send(:include, described_class) }
|
6
|
+
|
7
|
+
describe ".call" do
|
8
|
+
let(:context) { double(:context) }
|
9
|
+
let(:instance) { double(:instance, context: context) }
|
10
|
+
|
11
|
+
it "calls an instance with the given context" do
|
12
|
+
expect(interactor).to receive(:new).once.with(foo: "bar") { instance }
|
13
|
+
expect(instance).to receive(:run).once.with(no_args)
|
14
|
+
|
15
|
+
expect(interactor.call(foo: "bar")).to eq(context)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "provides a blank context if none is given" do
|
19
|
+
expect(interactor).to receive(:new).once.with({}) { instance }
|
20
|
+
expect(instance).to receive(:run).once.with(no_args)
|
21
|
+
|
22
|
+
expect(interactor.call).to eq(context)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe ".call!" do
|
27
|
+
let(:context) { double(:context) }
|
28
|
+
let(:instance) { double(:instance, context: context) }
|
29
|
+
|
30
|
+
it "calls an instance with the given context" do
|
31
|
+
expect(interactor).to receive(:new).once.with(foo: "bar") { instance }
|
32
|
+
expect(instance).to receive(:run!).once.with(no_args)
|
33
|
+
|
34
|
+
expect(interactor.call!(foo: "bar")).to eq(context)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "provides a blank context if none is given" do
|
38
|
+
expect(interactor).to receive(:new).once.with({}) { instance }
|
39
|
+
expect(instance).to receive(:run!).once.with(no_args)
|
40
|
+
|
41
|
+
expect(interactor.call!).to eq(context)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe ".new" do
|
46
|
+
let(:context) { double(:context) }
|
47
|
+
|
48
|
+
it "initializes a context" do
|
49
|
+
expect(Interactor::Schema::Context).to receive(:build).once.with({ foo: "bar" }, []) { context }
|
50
|
+
|
51
|
+
instance = interactor.new(foo: "bar")
|
52
|
+
|
53
|
+
expect(instance).to be_a(interactor)
|
54
|
+
expect(instance.context).to eq(context)
|
55
|
+
end
|
56
|
+
|
57
|
+
it "initializes a blank context if none is given" do
|
58
|
+
expect(Interactor::Schema::Context).to receive(:build).once.with({}, []) { context }
|
59
|
+
|
60
|
+
instance = interactor.new
|
61
|
+
|
62
|
+
expect(instance).to be_a(interactor)
|
63
|
+
expect(instance.context).to eq(context)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe "#run" do
|
68
|
+
let(:instance) { interactor.new }
|
69
|
+
|
70
|
+
it "runs the interactor" do
|
71
|
+
expect(instance).to receive(:run!).once.with(no_args)
|
72
|
+
|
73
|
+
instance.run
|
74
|
+
end
|
75
|
+
|
76
|
+
it "rescues failure" do
|
77
|
+
expect(instance).to receive(:run!).and_raise(Interactor::Failure)
|
78
|
+
|
79
|
+
expect {
|
80
|
+
instance.run
|
81
|
+
}.not_to raise_error
|
82
|
+
end
|
83
|
+
|
84
|
+
it "raises other errors" do
|
85
|
+
expect(instance).to receive(:run!).and_raise("foo")
|
86
|
+
|
87
|
+
expect {
|
88
|
+
instance.run
|
89
|
+
}.to raise_error("foo")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe "#run!" do
|
94
|
+
let(:instance) { interactor.new }
|
95
|
+
|
96
|
+
it "calls the interactor" do
|
97
|
+
expect(instance).to receive(:call).once.with(no_args)
|
98
|
+
|
99
|
+
instance.run!
|
100
|
+
end
|
101
|
+
|
102
|
+
it "raises failure" do
|
103
|
+
expect(instance).to receive(:run!).and_raise(Interactor::Failure)
|
104
|
+
|
105
|
+
expect {
|
106
|
+
instance.run!
|
107
|
+
}.to raise_error(Interactor::Failure)
|
108
|
+
end
|
109
|
+
|
110
|
+
it "raises other errors" do
|
111
|
+
expect(instance).to receive(:run!).and_raise("foo")
|
112
|
+
|
113
|
+
expect {
|
114
|
+
instance.run
|
115
|
+
}.to raise_error("foo")
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
describe "#call" do
|
120
|
+
let(:instance) { interactor.new }
|
121
|
+
|
122
|
+
it "exists" do
|
123
|
+
expect(instance).to respond_to(:call)
|
124
|
+
expect { instance.call }.not_to raise_error
|
125
|
+
expect { instance.method(:call) }.not_to raise_error
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe "#rollback" do
|
130
|
+
let(:instance) { interactor.new }
|
131
|
+
|
132
|
+
it "exists" do
|
133
|
+
expect(instance).to respond_to(:rollback)
|
134
|
+
expect { instance.rollback }.not_to raise_error
|
135
|
+
expect { instance.method(:rollback) }.not_to raise_error
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
metadata
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: interactor-schema
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bernardo Farah
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-01-31 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: interactor
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.13'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.13'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.1'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.1'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pry
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.10'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.10'
|
83
|
+
description: Interactor::Schema provides an enforcable Interactor Context
|
84
|
+
email:
|
85
|
+
- ber@bernardo.me
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".gitignore"
|
91
|
+
- ".rspec"
|
92
|
+
- ".travis.yml"
|
93
|
+
- Gemfile
|
94
|
+
- LICENSE.txt
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- bin/console
|
98
|
+
- bin/setup
|
99
|
+
- interactor-schema.gemspec
|
100
|
+
- lib/interactor/schema.rb
|
101
|
+
- lib/interactor/schema/context.rb
|
102
|
+
- lib/interactor/schema/context_extension.rb
|
103
|
+
- lib/interactor/schema/interactor_extension.rb
|
104
|
+
- lib/interactor/schema/version.rb
|
105
|
+
- spec/interactor/schema/context_spec.rb
|
106
|
+
- spec/interactor/schema/interactor_extension_spec.rb
|
107
|
+
- spec/interactor/schema_spec.rb
|
108
|
+
- spec/spec_helper.rb
|
109
|
+
- spec/support/lint.rb
|
110
|
+
homepage: https://github.com/berfarah/interactor-schema
|
111
|
+
licenses:
|
112
|
+
- MIT
|
113
|
+
metadata: {}
|
114
|
+
post_install_message:
|
115
|
+
rdoc_options: []
|
116
|
+
require_paths:
|
117
|
+
- lib
|
118
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '0'
|
123
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '0'
|
128
|
+
requirements: []
|
129
|
+
rubyforge_project:
|
130
|
+
rubygems_version: 2.5.1
|
131
|
+
signing_key:
|
132
|
+
specification_version: 4
|
133
|
+
summary: Enforce a schema with for Interactor Context
|
134
|
+
test_files:
|
135
|
+
- spec/interactor/schema/context_spec.rb
|
136
|
+
- spec/interactor/schema/interactor_extension_spec.rb
|
137
|
+
- spec/interactor/schema_spec.rb
|
138
|
+
- spec/spec_helper.rb
|
139
|
+
- spec/support/lint.rb
|