trailblazer 1.1.2 → 2.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +10 -7
- data/CHANGES.md +108 -0
- data/COMM-LICENSE +91 -0
- data/Gemfile +18 -4
- data/LICENSE.txt +7 -20
- data/README.md +55 -15
- data/Rakefile +21 -2
- data/draft-1.2.rb +7 -0
- data/lib/trailblazer.rb +17 -4
- data/lib/trailblazer/dsl.rb +47 -0
- data/lib/trailblazer/operation/auto_inject.rb +47 -0
- data/lib/trailblazer/operation/builder.rb +18 -18
- data/lib/trailblazer/operation/callback.rb +31 -38
- data/lib/trailblazer/operation/contract.rb +46 -0
- data/lib/trailblazer/operation/controller.rb +45 -27
- data/lib/trailblazer/operation/guard.rb +24 -0
- data/lib/trailblazer/operation/model.rb +41 -33
- data/lib/trailblazer/operation/nested.rb +43 -0
- data/lib/trailblazer/operation/params.rb +13 -0
- data/lib/trailblazer/operation/persist.rb +13 -0
- data/lib/trailblazer/operation/policy.rb +26 -72
- data/lib/trailblazer/operation/present.rb +19 -0
- data/lib/trailblazer/operation/procedural/contract.rb +15 -0
- data/lib/trailblazer/operation/procedural/validate.rb +22 -0
- data/lib/trailblazer/operation/pundit.rb +42 -0
- data/lib/trailblazer/operation/representer.rb +25 -92
- data/lib/trailblazer/operation/rescue.rb +23 -0
- data/lib/trailblazer/operation/resolver.rb +18 -24
- data/lib/trailblazer/operation/validate.rb +50 -0
- data/lib/trailblazer/operation/wrap.rb +37 -0
- data/lib/trailblazer/version.rb +1 -1
- data/test/{operation/controller_test.rb → controller_test.rb} +8 -4
- data/test/docs/auto_inject_test.rb +30 -0
- data/test/docs/contract_test.rb +429 -0
- data/test/docs/dry_test.rb +31 -0
- data/test/docs/guard_test.rb +143 -0
- data/test/docs/nested_test.rb +117 -0
- data/test/docs/policy_test.rb +2 -0
- data/test/docs/pundit_test.rb +109 -0
- data/test/docs/representer_test.rb +268 -0
- data/test/docs/rescue_test.rb +153 -0
- data/test/docs/wrap_test.rb +174 -0
- data/test/gemfiles/Gemfile.ruby-1.9 +3 -0
- data/test/gemfiles/Gemfile.ruby-2.0 +12 -0
- data/test/gemfiles/Gemfile.ruby-2.3 +12 -0
- data/test/module_test.rb +22 -15
- data/test/operation/builder_test.rb +66 -18
- data/test/operation/callback_test.rb +70 -0
- data/test/operation/contract_test.rb +385 -15
- data/test/operation/dsl/callback_test.rb +18 -30
- data/test/operation/dsl/contract_test.rb +209 -19
- data/test/operation/dsl/representer_test.rb +42 -15
- data/test/operation/guard_test.rb +1 -147
- data/test/operation/model_test.rb +105 -0
- data/test/operation/params_test.rb +36 -0
- data/test/operation/persist_test.rb +44 -0
- data/test/operation/pipedream_test.rb +59 -0
- data/test/operation/pipetree_test.rb +104 -0
- data/test/operation/present_test.rb +24 -0
- data/test/operation/pundit_test.rb +104 -0
- data/test/{representer_test.rb → operation/representer_test.rb} +58 -42
- data/test/operation/resolver_test.rb +34 -70
- data/test/operation_test.rb +57 -189
- data/test/test_helper.rb +23 -3
- data/trailblazer.gemspec +8 -7
- metadata +91 -59
- data/gemfiles/Gemfile.rails.lock +0 -130
- data/gemfiles/Gemfile.reform-2.0 +0 -6
- data/gemfiles/Gemfile.reform-2.1 +0 -7
- data/lib/trailblazer/autoloading.rb +0 -15
- data/lib/trailblazer/endpoint.rb +0 -31
- data/lib/trailblazer/operation.rb +0 -175
- data/lib/trailblazer/operation/collection.rb +0 -6
- data/lib/trailblazer/operation/dispatch.rb +0 -3
- data/lib/trailblazer/operation/model/dsl.rb +0 -29
- data/lib/trailblazer/operation/model/external.rb +0 -34
- data/lib/trailblazer/operation/policy/guard.rb +0 -35
- data/lib/trailblazer/operation/uploaded_file.rb +0 -77
- data/test/callback_test.rb +0 -104
- data/test/collection_test.rb +0 -57
- data/test/model_test.rb +0 -148
- data/test/operation/external_model_test.rb +0 -71
- data/test/operation/policy_test.rb +0 -97
- data/test/operation/reject_test.rb +0 -34
- data/test/rollback_test.rb +0 -47
@@ -1,35 +0,0 @@
|
|
1
|
-
module Trailblazer
|
2
|
-
# Policy::Guard is a very simple policy implementation.
|
3
|
-
# It adds #evaluate_policy to Operation#setup! and calls whatever
|
4
|
-
# you provided to ::policy.
|
5
|
-
#
|
6
|
-
# http://trailblazer.to/gems/operation/policy.html#guard
|
7
|
-
module Operation::Policy
|
8
|
-
module Guard
|
9
|
-
def self.included(includer)
|
10
|
-
includer.extend(DSL) # Provides ::policy(CallableObject)
|
11
|
-
includer.extend(ClassMethods)
|
12
|
-
includer.send(:include, Setup)
|
13
|
-
end
|
14
|
-
|
15
|
-
module ClassMethods
|
16
|
-
def policy(callable=nil, &block)
|
17
|
-
self.policy_config = Uber::Options::Value.new(callable || block)
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
def evaluate_policy(params)
|
22
|
-
call_policy(params) or raise policy_exception
|
23
|
-
end
|
24
|
-
|
25
|
-
# Override if you want your own policy invocation, e.g. with more args.
|
26
|
-
def call_policy(params)
|
27
|
-
self.class.policy_config.(self, params)
|
28
|
-
end
|
29
|
-
|
30
|
-
def policy_exception
|
31
|
-
NotAuthorizedError.new
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
@@ -1,77 +0,0 @@
|
|
1
|
-
require 'trailblazer/operation'
|
2
|
-
require 'action_dispatch/http/upload'
|
3
|
-
require 'tempfile'
|
4
|
-
|
5
|
-
module Trailblazer
|
6
|
-
# TODO: document:
|
7
|
-
# to_hash
|
8
|
-
# from_hash
|
9
|
-
# initialize/tmp_dir
|
10
|
-
class Operation::UploadedFile
|
11
|
-
def initialize(uploaded, options={})
|
12
|
-
@uploaded = uploaded
|
13
|
-
@options = options
|
14
|
-
@tmp_dir = options[:tmp_dir]
|
15
|
-
end
|
16
|
-
|
17
|
-
def to_hash
|
18
|
-
path = persist!
|
19
|
-
|
20
|
-
hash = {
|
21
|
-
filename: @uploaded.original_filename,
|
22
|
-
type: @uploaded.content_type,
|
23
|
-
tempfile_path: path
|
24
|
-
}
|
25
|
-
|
26
|
-
cleanup!
|
27
|
-
|
28
|
-
hash
|
29
|
-
end
|
30
|
-
|
31
|
-
# Returns a ActionDispatch::Http::UploadedFile as if the upload was in the same request.
|
32
|
-
def self.from_hash(hash)
|
33
|
-
suffix = File.extname(hash[:filename])
|
34
|
-
|
35
|
-
# we need to create a Tempfile to make Http::UploadedFile work.
|
36
|
-
tmp = Tempfile.new(["bla", suffix]) # always force file suffix to avoid problems with imagemagick etc.
|
37
|
-
file = File.open(hash[:tempfile_path])# doesn't close automatically :( # fixme: introduce strategy (Tempfile:=>slow, File:=> hopefully less memory footprint)
|
38
|
-
tmp.write(file.read) # DISCUSS: We need Tempfile.new(<File>) to avoid this slow and memory-consuming mechanics.
|
39
|
-
|
40
|
-
file.close # TODO: can we test that?
|
41
|
-
File.unlink(file)
|
42
|
-
|
43
|
-
ActionDispatch::Http::UploadedFile.new(hash.merge(tempfile: tmp))
|
44
|
-
end
|
45
|
-
|
46
|
-
private
|
47
|
-
attr_reader :tmp_dir
|
48
|
-
|
49
|
-
# convert Tempfile from Rails upload into persistent "temp" file so it is available in workers.
|
50
|
-
def persist!
|
51
|
-
path = @uploaded.path # original Tempfile path (from Rails).
|
52
|
-
path = path_with_tmp_dir(path)
|
53
|
-
|
54
|
-
path = path + "_trailblazer_upload"
|
55
|
-
|
56
|
-
FileUtils.mv(@uploaded.path, path) # move Rails upload file into persistent `path`.
|
57
|
-
path
|
58
|
-
end
|
59
|
-
|
60
|
-
def path_with_tmp_dir(path)
|
61
|
-
return path unless tmp_dir # if tmp_dir set, create path in it.
|
62
|
-
|
63
|
-
@with_tmp_dir = Tempfile.new(File.basename(path), tmp_dir)
|
64
|
-
@with_tmp_dir.path # use Tempfile to create nested dirs (os-dependent.)
|
65
|
-
end
|
66
|
-
|
67
|
-
def delete!(file)
|
68
|
-
file.close
|
69
|
-
file.unlink # the Rails uploaded file is already unlinked since moved.
|
70
|
-
end
|
71
|
-
|
72
|
-
def cleanup!
|
73
|
-
delete!(@uploaded.tempfile) if @uploaded.respond_to?(:tempfile) # this is Rails' uploaded file, not sure if we need to do that. in 3.2, we don't have UploadedFile#close, yet.
|
74
|
-
delete!(@with_tmp_dir) if @with_tmp_dir # we used that file to create a tmp file path below tmp_dir.
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
data/test/callback_test.rb
DELETED
@@ -1,104 +0,0 @@
|
|
1
|
-
require 'test_helper'
|
2
|
-
require "trailblazer/operation/callback"
|
3
|
-
|
4
|
-
# callbacks are tested in Disposable::Callback::Group.
|
5
|
-
class OperationCallbackTest < MiniTest::Spec
|
6
|
-
Song = Struct.new(:name)
|
7
|
-
|
8
|
-
class Create < Trailblazer::Operation
|
9
|
-
include Trailblazer::Operation::Callback
|
10
|
-
|
11
|
-
contract do
|
12
|
-
property :name
|
13
|
-
end
|
14
|
-
|
15
|
-
callback do
|
16
|
-
on_change :notify_me!
|
17
|
-
on_change :notify_you!
|
18
|
-
end
|
19
|
-
|
20
|
-
# TODO: always dispatch, pass params.
|
21
|
-
|
22
|
-
def process(params)
|
23
|
-
@model = Song.new
|
24
|
-
|
25
|
-
validate(params, @model) do
|
26
|
-
callback!
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
def dispatched
|
31
|
-
@dispatched ||= []
|
32
|
-
end
|
33
|
-
|
34
|
-
private
|
35
|
-
def notify_me!(*)
|
36
|
-
dispatched << :notify_me!
|
37
|
-
end
|
38
|
-
|
39
|
-
def notify_you!(*)
|
40
|
-
dispatched << :notify_you!
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
|
45
|
-
class Update < Create
|
46
|
-
# TODO: allow skipping groups.
|
47
|
-
# skip_dispatch :notify_me!
|
48
|
-
|
49
|
-
callback do
|
50
|
-
remove! :on_change, :notify_me!
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
|
55
|
-
it "invokes all callbacks" do
|
56
|
-
op = Create.({"name"=>"Keep On Running"})
|
57
|
-
op.dispatched.must_equal [:notify_me!, :notify_you!]
|
58
|
-
end
|
59
|
-
|
60
|
-
it "does not invoke removed callbacks" do
|
61
|
-
op = Update.({"name"=>"Keep On Running"})
|
62
|
-
op.dispatched.must_equal [:notify_you!]
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
# TODO: remove in 1.2.
|
67
|
-
require "trailblazer/operation/dispatch"
|
68
|
-
class OperationDeprecatedDispatchTest < MiniTest::Spec
|
69
|
-
Song = Struct.new(:name)
|
70
|
-
|
71
|
-
class Create < Trailblazer::Operation
|
72
|
-
include Trailblazer::Operation::Dispatch
|
73
|
-
|
74
|
-
contract do
|
75
|
-
property :name
|
76
|
-
end
|
77
|
-
|
78
|
-
callback do
|
79
|
-
on_change :notify_me!
|
80
|
-
end
|
81
|
-
|
82
|
-
def process(params)
|
83
|
-
@model = Song.new
|
84
|
-
|
85
|
-
validate(params, @model) do
|
86
|
-
dispatch!
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
def dispatched
|
91
|
-
@dispatched ||= []
|
92
|
-
end
|
93
|
-
|
94
|
-
private
|
95
|
-
def notify_me!(*)
|
96
|
-
dispatched << :notify_me!
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
it "invokes all callbacks [deprecated]" do
|
101
|
-
op = Create.({"name"=>"Keep On Running"})
|
102
|
-
op.dispatched.must_equal [:notify_me!]
|
103
|
-
end
|
104
|
-
end
|
data/test/collection_test.rb
DELETED
@@ -1,57 +0,0 @@
|
|
1
|
-
require "test_helper"
|
2
|
-
require "trailblazer/operation/collection"
|
3
|
-
require "trailblazer/operation/model"
|
4
|
-
|
5
|
-
class CollectionTest < MiniTest::Spec
|
6
|
-
Song = Struct.new(:title, :id) do
|
7
|
-
class << self
|
8
|
-
attr_accessor :all_records
|
9
|
-
|
10
|
-
def all
|
11
|
-
all_records
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
|
17
|
-
class CreateOperation < Trailblazer::Operation
|
18
|
-
include Model
|
19
|
-
model Song
|
20
|
-
action :create
|
21
|
-
|
22
|
-
contract do
|
23
|
-
property :title
|
24
|
-
validates :title, presence: true
|
25
|
-
end
|
26
|
-
|
27
|
-
def process(params)
|
28
|
-
validate(params[:song]) do |f|
|
29
|
-
f.sync
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
class FetchCollectionOperation < CreateOperation
|
35
|
-
include Trailblazer::Operation::Collection
|
36
|
-
|
37
|
-
model Song
|
38
|
-
|
39
|
-
contract do
|
40
|
-
property :title
|
41
|
-
end
|
42
|
-
|
43
|
-
def model!(params)
|
44
|
-
Song.all
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
# ::present.
|
49
|
-
it do
|
50
|
-
Song.all_records = [
|
51
|
-
CreateOperation.(song: {title: "Blue Rondo a la Turk"}).model,
|
52
|
-
CreateOperation.(song: {title: "Mercy Day For Mr. Vengeance"}).model
|
53
|
-
]
|
54
|
-
op = FetchCollectionOperation.present(user_id: 0)
|
55
|
-
op.model.must_equal Song.all_records
|
56
|
-
end
|
57
|
-
end
|
data/test/model_test.rb
DELETED
@@ -1,148 +0,0 @@
|
|
1
|
-
require 'test_helper'
|
2
|
-
require 'trailblazer/operation'
|
3
|
-
|
4
|
-
class ModelTest < MiniTest::Spec
|
5
|
-
Song = Struct.new(:title, :id) do
|
6
|
-
class << self
|
7
|
-
attr_accessor :find_result # TODO: eventually, replace with AR test.
|
8
|
-
attr_accessor :all_records
|
9
|
-
|
10
|
-
def find(id)
|
11
|
-
find_result
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
class CreateOperation < Trailblazer::Operation
|
17
|
-
include Model
|
18
|
-
model Song
|
19
|
-
action :create
|
20
|
-
|
21
|
-
contract do
|
22
|
-
property :title
|
23
|
-
validates :title, presence: true
|
24
|
-
end
|
25
|
-
|
26
|
-
def process(params)
|
27
|
-
validate(params[:song]) do |f|
|
28
|
-
f.sync
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
|
34
|
-
# creates model for you.
|
35
|
-
it { CreateOperation.(song: {title: "Blue Rondo a la Turk"}).model.title.must_equal "Blue Rondo a la Turk" }
|
36
|
-
# exposes #model.
|
37
|
-
it { CreateOperation.(song: {title: "Blue Rondo a la Turk"}).model.must_be_instance_of Song }
|
38
|
-
|
39
|
-
class ModifyingCreateOperation < CreateOperation
|
40
|
-
def process(params)
|
41
|
-
model.instance_eval { def genre; "Punkrock"; end }
|
42
|
-
|
43
|
-
validate(params[:song]) do |f|
|
44
|
-
f.sync
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
# lets you modify model.
|
50
|
-
it { ModifyingCreateOperation.(song: {title: "Blue Rondo a la Turk"}).model.title.must_equal "Blue Rondo a la Turk" }
|
51
|
-
it { ModifyingCreateOperation.(song: {title: "Blue Rondo a la Turk"}).model.genre.must_equal "Punkrock" }
|
52
|
-
|
53
|
-
# Update
|
54
|
-
class UpdateOperation < CreateOperation
|
55
|
-
action :update
|
56
|
-
end
|
57
|
-
|
58
|
-
# finds model and updates.
|
59
|
-
it do
|
60
|
-
song = CreateOperation.(song: {title: "Anchor End"}).model
|
61
|
-
Song.find_result = song
|
62
|
-
|
63
|
-
UpdateOperation.(id: song.id, song: {title: "The Rip"}).model.title.must_equal "The Rip"
|
64
|
-
song.title.must_equal "The Rip"
|
65
|
-
end
|
66
|
-
|
67
|
-
# Find == Update
|
68
|
-
class FindOperation < CreateOperation
|
69
|
-
action :find
|
70
|
-
end
|
71
|
-
|
72
|
-
# finds model and updates.
|
73
|
-
it do
|
74
|
-
song = CreateOperation.(song: {title: "Anchor End"}).model
|
75
|
-
Song.find_result = song
|
76
|
-
|
77
|
-
FindOperation.(id: song.id, song: {title: "The Rip"}).model.title.must_equal "The Rip"
|
78
|
-
song.title.must_equal "The Rip"
|
79
|
-
end
|
80
|
-
|
81
|
-
|
82
|
-
class DefaultCreateOperation < Trailblazer::Operation
|
83
|
-
include Model
|
84
|
-
model Song
|
85
|
-
|
86
|
-
def process(params)
|
87
|
-
self
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
# uses :create as default if not set via ::action.
|
92
|
-
it { DefaultCreateOperation.({}).model.must_equal Song.new }
|
93
|
-
|
94
|
-
# model Song, :action
|
95
|
-
class ModelUpdateOperation < CreateOperation
|
96
|
-
model Song, :update
|
97
|
-
end
|
98
|
-
|
99
|
-
# allows ::model, :action.
|
100
|
-
it do
|
101
|
-
Song.find_result = song = Song.new
|
102
|
-
ModelUpdateOperation.({id: 1, song: {title: "Mercy Day For Mr. Vengeance"}}).model.must_equal song
|
103
|
-
end
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
# Op#setup_model!
|
108
|
-
class SetupModelOperation < CreateOperation
|
109
|
-
def setup_model!(params)
|
110
|
-
model.instance_eval { @params = params; def params; @params.to_s; end }
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
it { SetupModelOperation.(song: {title: "Emily Kane"}).model.params.must_equal "{:song=>{:title=>\"Emily Kane\"}}" }
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
# no call to ::model raises error.
|
119
|
-
class NoModelOperation < Trailblazer::Operation
|
120
|
-
include Model
|
121
|
-
|
122
|
-
def process(params)
|
123
|
-
self
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
# uses :create as default if not set via ::action.
|
128
|
-
it { assert_raises(RuntimeError){ NoModelOperation.({}) } }
|
129
|
-
|
130
|
-
# allow passing validate(params, model, contract_class)
|
131
|
-
class OperationWithPrivateContract < Trailblazer::Operation
|
132
|
-
include Model
|
133
|
-
model Song
|
134
|
-
|
135
|
-
class Contract < Reform::Form
|
136
|
-
property :title
|
137
|
-
end
|
138
|
-
|
139
|
-
def process(params)
|
140
|
-
validate(params[:song], model, Contract) do |f|
|
141
|
-
f.sync
|
142
|
-
end
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
# uses private Contract class.
|
147
|
-
it { OperationWithPrivateContract.(song: {title: "Blue Rondo a la Turk"}).model.title.must_equal "Blue Rondo a la Turk" }
|
148
|
-
end
|
@@ -1,71 +0,0 @@
|
|
1
|
-
require "test_helper"
|
2
|
-
require "trailblazer/operation/model/external"
|
3
|
-
|
4
|
-
class ExternalModelTest < MiniTest::Spec
|
5
|
-
Song = Struct.new(:title, :id) do
|
6
|
-
class << self
|
7
|
-
attr_accessor :find_result # TODO: eventually, replace with AR test.
|
8
|
-
attr_accessor :all_records
|
9
|
-
|
10
|
-
def find(id)
|
11
|
-
find_result.tap do |song|
|
12
|
-
song.id = id
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end # FIXME: use from CrudTest.
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
class Bla < Trailblazer::Operation
|
22
|
-
include Model::External
|
23
|
-
model Song, :update
|
24
|
-
|
25
|
-
def process(params)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
let (:song) { Song.new("Numbers") }
|
30
|
-
|
31
|
-
before do
|
32
|
-
Song.find_result = song
|
33
|
-
end
|
34
|
-
|
35
|
-
# ::model!
|
36
|
-
it do
|
37
|
-
Bla.model!(id: 1).must_equal song
|
38
|
-
song.id.must_equal 1
|
39
|
-
end
|
40
|
-
|
41
|
-
# call style.
|
42
|
-
it do
|
43
|
-
Bla.(id: 2).model.must_equal song
|
44
|
-
song.id.must_equal 2
|
45
|
-
end
|
46
|
-
|
47
|
-
# #present.
|
48
|
-
it do
|
49
|
-
Bla.present({}).model.must_equal song
|
50
|
-
end
|
51
|
-
|
52
|
-
|
53
|
-
class OpWithBuilder < Bla
|
54
|
-
class A < self
|
55
|
-
end
|
56
|
-
|
57
|
-
builds -> (model, params) do
|
58
|
-
return A if model.id == 1 and params[:user] == 2
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
describe "::builds args" do
|
63
|
-
it do
|
64
|
-
OpWithBuilder.(id: 1, user: "different").must_be_instance_of OpWithBuilder
|
65
|
-
end
|
66
|
-
|
67
|
-
it do
|
68
|
-
OpWithBuilder.(id: 1, user: 2).must_be_instance_of OpWithBuilder::A
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|