trailblazer 2.0.0 → 2.0.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.
- checksums.yaml +4 -4
- data/CHANGES.md +13 -0
- data/COMM-LICENSE +21 -21
- data/Gemfile +3 -0
- data/README.md +15 -77
- data/Rakefile +1 -2
- data/doc/operation-2017.png +0 -0
- data/lib/trailblazer.rb +0 -1
- data/lib/trailblazer/operation/callback.rb +10 -19
- data/lib/trailblazer/operation/contract.rb +7 -8
- data/lib/trailblazer/operation/guard.rb +4 -13
- data/lib/trailblazer/operation/model.rb +30 -32
- data/lib/trailblazer/operation/nested.rb +21 -32
- data/lib/trailblazer/operation/persist.rb +4 -8
- data/lib/trailblazer/operation/policy.rb +5 -15
- data/lib/trailblazer/operation/pundit.rb +8 -17
- data/lib/trailblazer/operation/rescue.rb +17 -17
- data/lib/trailblazer/operation/validate.rb +34 -21
- data/lib/trailblazer/operation/wrap.rb +12 -25
- data/lib/trailblazer/version.rb +1 -1
- data/test/docs/dry_test.rb +4 -4
- data/test/docs/fast_test.rb +164 -0
- data/test/docs/guard_test.rb +2 -2
- data/test/docs/macro_test.rb +36 -0
- data/test/docs/model_test.rb +52 -0
- data/test/docs/nested_test.rb +101 -4
- data/test/docs/operation_test.rb +225 -1
- data/test/docs/pundit_test.rb +18 -17
- data/test/docs/rescue_test.rb +3 -3
- data/test/docs/wrap_test.rb +1 -0
- data/test/operation/callback_test.rb +5 -5
- data/test/operation/contract_test.rb +30 -19
- data/test/operation/dsl/callback_test.rb +2 -2
- data/test/operation/dsl/contract_test.rb +4 -4
- data/test/operation/model_test.rb +12 -57
- data/test/operation/persist_test.rb +7 -7
- data/test/operation/pipedream_test.rb +9 -9
- data/test/operation/pipetree_test.rb +5 -5
- data/test/operation/pundit_test.rb +7 -7
- data/test/operation/resolver_test.rb +47 -47
- data/trailblazer.gemspec +1 -1
- metadata +18 -11
- data/lib/trailblazer/operation/builder.rb +0 -24
- data/lib/trailblazer/operation/resolver.rb +0 -22
- data/test/controller_test.rb +0 -115
- data/test/operation/builder_test.rb +0 -89
@@ -1,43 +1,32 @@
|
|
1
1
|
class Trailblazer::Operation
|
2
|
-
|
3
|
-
|
4
|
-
# superinternal API is not entirely decided, yet.
|
5
|
-
def self.import!(operation, import, step)
|
6
|
-
import.(:&, ->(input, options) {
|
7
|
-
result = step._call(*options.to_runtime_data)
|
2
|
+
def self.Nested(step)
|
3
|
+
step = Nested.for(step)
|
8
4
|
|
9
|
-
|
10
|
-
options[k] = v
|
11
|
-
end
|
12
|
-
|
13
|
-
result.success? # DISCUSS: what if we could simply return the result object here?
|
14
|
-
}, {} )
|
15
|
-
end
|
5
|
+
[ step, {} ]
|
16
6
|
end
|
17
7
|
|
18
|
-
|
8
|
+
module Nested
|
9
|
+
def self.proc_for(step)
|
10
|
+
if step.is_a?(Class) && step <= Trailblazer::Operation # interestingly, with < we get a weird nil exception. bug in Ruby?
|
11
|
+
proc = ->(input, options) { step._call(*options.to_runtime_data) }
|
12
|
+
else
|
13
|
+
option = Option::KW.(step)
|
14
|
+
proc = ->(input, options) { option.(input, options).(*options.to_runtime_data) }
|
15
|
+
end
|
16
|
+
end
|
19
17
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
18
|
+
# Please note that the instance_variable_get are here on purpose since the
|
19
|
+
# superinternal API is not entirely decided, yet.
|
20
|
+
def self.for(step)
|
21
|
+
proc = proc_for(step)
|
24
22
|
|
25
|
-
|
26
|
-
|
27
|
-
res = nested_pipe.call
|
28
|
-
res.first == ::Pipetree::Flow::Right # FIXME.
|
29
|
-
rescue *exceptions => exception
|
30
|
-
handler.call(operation, exception, options)
|
31
|
-
#options["result.model.find"] = "argh! because #{exception.class}"
|
32
|
-
false
|
33
|
-
end
|
34
|
-
}
|
23
|
+
->(input, options) do
|
24
|
+
result = proc.(input, options) # TODO: what about containers?
|
35
25
|
|
36
|
-
|
37
|
-
|
26
|
+
result.instance_variable_get(:@data).to_mutable_data.each { |k,v| options[k] = v }
|
27
|
+
result.success? # DISCUSS: what if we could simply return the result object here?
|
28
|
+
end
|
38
29
|
end
|
39
30
|
end
|
40
|
-
|
41
|
-
DSL.macro!(:Rescue, Rescue)
|
42
31
|
end
|
43
32
|
|
@@ -1,14 +1,10 @@
|
|
1
1
|
class Trailblazer::Operation
|
2
2
|
module Contract
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
def self.Persist(method: :save, name: "default")
|
4
|
+
path = "contract.#{name}"
|
5
|
+
step = ->(input, options) { options[path].send(method) }
|
6
6
|
|
7
|
-
|
8
|
-
name: "persist.save")
|
9
|
-
end
|
7
|
+
[ step, name: "persist.save" ]
|
10
8
|
end
|
11
9
|
end
|
12
|
-
|
13
|
-
DSL.macro!(:Persist, Contract::Persist, Contract.singleton_class) # Contract::Persist()
|
14
10
|
end
|
@@ -3,8 +3,6 @@ class Trailblazer::Operation
|
|
3
3
|
# Step: This generically `call`s a policy and then pushes its result to `options`.
|
4
4
|
# You can use any callable object as a policy with this step.
|
5
5
|
class Eval
|
6
|
-
include Uber::Callable
|
7
|
-
|
8
6
|
def initialize(name:nil, path:nil)
|
9
7
|
@name = name
|
10
8
|
@path = path
|
@@ -24,22 +22,14 @@ class Trailblazer::Operation
|
|
24
22
|
|
25
23
|
# Adds the `yield` result to the pipe and treats it like a
|
26
24
|
# policy-compatible object at runtime.
|
27
|
-
def self.
|
25
|
+
def self.step(condition, options, &block)
|
28
26
|
name = options[:name]
|
29
|
-
path =
|
30
|
-
|
31
|
-
configure!(operation, import, options, &block)
|
27
|
+
path = "policy.#{name}.eval"
|
32
28
|
|
33
|
-
|
34
|
-
|
35
|
-
insert_options.merge(name: path)
|
36
|
-
)
|
37
|
-
end
|
29
|
+
step = Eval.new( name: name, path: path )
|
30
|
+
step = Pipetree::Step.new(step, path => condition)
|
38
31
|
|
39
|
-
|
40
|
-
def self.configure!(operation, import, options)
|
41
|
-
# configure class level.
|
42
|
-
operation[ options[:path] ] = yield
|
32
|
+
[ step, name: path ]
|
43
33
|
end
|
44
34
|
end
|
45
35
|
end
|
@@ -1,10 +1,10 @@
|
|
1
1
|
class Trailblazer::Operation
|
2
2
|
module Policy
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
end
|
3
|
+
def self.Pundit(policy_class, action, name: :default)
|
4
|
+
Policy.step(Pundit.build(policy_class, action), name: name)
|
5
|
+
end
|
7
6
|
|
7
|
+
module Pundit
|
8
8
|
def self.build(*args, &block)
|
9
9
|
Condition.new(*args, &block)
|
10
10
|
end
|
@@ -16,14 +16,14 @@ class Trailblazer::Operation
|
|
16
16
|
end
|
17
17
|
|
18
18
|
# Instantiate the actual policy object, and call it.
|
19
|
-
def call(
|
20
|
-
policy = build_policy(
|
19
|
+
def call(options)
|
20
|
+
policy = build_policy(options) # this translates to Pundit interface.
|
21
21
|
result!(policy.send(@action), policy)
|
22
22
|
end
|
23
23
|
|
24
24
|
private
|
25
|
-
def build_policy(
|
26
|
-
@policy_class.new(
|
25
|
+
def build_policy(options)
|
26
|
+
@policy_class.new(options["current_user"], options["model"])
|
27
27
|
end
|
28
28
|
|
29
29
|
def result!(success, policy)
|
@@ -34,14 +34,5 @@ class Trailblazer::Operation
|
|
34
34
|
end
|
35
35
|
end
|
36
36
|
end
|
37
|
-
|
38
|
-
def self.Pundit(policy, condition, name: :default, &block)
|
39
|
-
options = {
|
40
|
-
name: name,
|
41
|
-
path: "policy.#{name}.eval",
|
42
|
-
}
|
43
|
-
|
44
|
-
[Pundit, [policy, condition, options], block]
|
45
|
-
end
|
46
37
|
end
|
47
38
|
end
|
@@ -1,25 +1,25 @@
|
|
1
1
|
class Trailblazer::Operation
|
2
|
-
|
3
|
-
|
2
|
+
def self.Rescue(*exceptions, handler: Rescue::Noop, &block)
|
3
|
+
exceptions = [StandardError] unless exceptions.any?
|
4
|
+
handler = Option.(handler)
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
|
6
|
+
rescue_block = ->(options, operation, *, &nested_pipe) {
|
7
|
+
begin
|
8
|
+
res = nested_pipe.call
|
9
|
+
res.first == ::Pipetree::Railway::Right # FIXME.
|
10
|
+
rescue *exceptions => exception
|
11
|
+
handler.call(operation, exception, options)
|
12
|
+
false
|
13
|
+
end
|
14
|
+
}
|
8
15
|
|
9
|
-
|
10
|
-
begin
|
11
|
-
res = nested_pipe.call
|
12
|
-
res.first == ::Pipetree::Flow::Right # FIXME.
|
13
|
-
rescue *exceptions => exception
|
14
|
-
handler.call(operation, exception, options)
|
15
|
-
false
|
16
|
-
end
|
17
|
-
}
|
16
|
+
step, _ = Wrap(rescue_block, &block)
|
18
17
|
|
19
|
-
|
20
|
-
end
|
18
|
+
[ step, name: "Rescue:#{block.source_location.last}" ]
|
21
19
|
end
|
22
20
|
|
23
|
-
|
21
|
+
module Rescue
|
22
|
+
Noop = ->(*) {}
|
23
|
+
end
|
24
24
|
end
|
25
25
|
|
@@ -1,36 +1,51 @@
|
|
1
1
|
class Trailblazer::Operation
|
2
2
|
module Contract
|
3
|
+
Railway = Pipetree::Railway
|
4
|
+
|
3
5
|
# result.contract = {..}
|
4
6
|
# result.contract.errors = {..}
|
5
7
|
# Deviate to left track if optional key is not found in params.
|
6
8
|
# Deviate to left if validation result falsey.
|
9
|
+
def self.Validate(skip_extract:false, name: "default", representer:false, key: nil) # DISCUSS: should we introduce something like Validate::Deserializer?
|
10
|
+
params_path = "contract.#{name}.params" # extract_params! save extracted params here.
|
11
|
+
|
12
|
+
return Validate::Call(name: name, representer: representer, params_path: params_path) if skip_extract || representer
|
13
|
+
|
14
|
+
extract_step, extract_options = Validate::Extract(key: key, params_path: params_path)
|
15
|
+
validate_step, validate_options = Validate::Call(name: name, representer: representer, params_path: params_path)
|
16
|
+
|
17
|
+
pipe = Railway.new # TODO: make nested pipes simpler.
|
18
|
+
.add(Railway::Right, Railway.&(extract_step), extract_options)
|
19
|
+
.add(Railway::Right, Railway.&(validate_step), validate_options)
|
20
|
+
|
21
|
+
step = ->(input, options) { pipe.(input, options).first <= Railway::Right }
|
22
|
+
|
23
|
+
[step, name: "contract.#{name}.validate"]
|
24
|
+
end
|
25
|
+
|
7
26
|
module Validate
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
27
|
+
# Macro: extract the contract's input from params by reading `:key`.
|
28
|
+
def self.Extract(key:nil, params_path:nil)
|
29
|
+
# TODO: introduce nested pipes and pass composed input instead.
|
30
|
+
step = ->(input, options) do
|
31
|
+
options[params_path] = key ? options["params"][key] : options["params"]
|
12
32
|
end
|
13
33
|
|
14
|
-
|
34
|
+
[ step, name: params_path ]
|
35
|
+
end
|
15
36
|
|
16
|
-
|
17
|
-
|
37
|
+
# Macro: Validates contract `:name`.
|
38
|
+
def self.Call(name:"default", representer:false, params_path:nil)
|
39
|
+
step = ->(input, options) {
|
40
|
+
validate!(options, name: name, representer: options["representer.#{name}.class"], params_path: params_path)
|
41
|
+
}
|
18
42
|
|
19
|
-
|
20
|
-
# DISCUSS: should we pass the representer here, or do that in #validate! i'm still mulling over what's the best, most generic approach.
|
21
|
-
import.(:&, ->(operation, options) do
|
22
|
-
validate!(operation, options, name: name, representer: options["representer.#{name}.class"], key: key, params_path: params_path)
|
23
|
-
end,
|
24
|
-
name: "contract.#{name}.validate", # visible name of the pipe step.
|
25
|
-
)
|
26
|
-
end
|
43
|
+
step = Pipetree::Step.new( step, "representer.#{name}.class" => representer )
|
27
44
|
|
28
|
-
|
29
|
-
# TODO: introduce nested pipes and pass composed input instead.
|
30
|
-
options[path] = key ? options["params"][key] : options["params"]
|
45
|
+
[ step, name: "contract.#{name}.call" ]
|
31
46
|
end
|
32
47
|
|
33
|
-
def self.validate!(
|
48
|
+
def self.validate!(options, name:nil, representer:false, from: "document", params_path:nil)
|
34
49
|
path = "contract.#{name}"
|
35
50
|
contract = options[path]
|
36
51
|
|
@@ -50,6 +65,4 @@ class Trailblazer::Operation
|
|
50
65
|
end
|
51
66
|
end
|
52
67
|
end
|
53
|
-
|
54
|
-
DSL.macro!(:Validate, Contract::Validate, Contract.singleton_class)
|
55
68
|
end
|
@@ -1,35 +1,22 @@
|
|
1
1
|
class Trailblazer::Operation
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
# DISCUSS: don't instance_exec when |pipe| given?
|
7
|
-
# yield pipe_api # create the nested pipe.
|
8
|
-
pipe_api.instance_exec(&block) # evaluate the nested pipe. this gets written onto `pipe`.
|
9
|
-
|
10
|
-
import.(:&, ->(input, options) { wrap.(options, input, pipe, & ->{ pipe.(input, options) }) }, _options)
|
11
|
-
end
|
2
|
+
def self.Wrap(wrap, &block)
|
3
|
+
operation = Class.new(Trailblazer::Operation) # DISCUSS: Trailblazer::Operation.inherit(skip_operation.new: true)
|
4
|
+
# DISCUSS: don't instance_exec when |pipe| given?
|
5
|
+
operation.instance_exec(&block) # evaluate the nested pipe.
|
12
6
|
|
13
|
-
|
14
|
-
|
15
|
-
include Pipetree::DSL::Macros
|
7
|
+
pipe = operation["pipetree"]
|
8
|
+
pipe.add(nil, nil, {delete: "operation.new"}) # TODO: make this a bit more elegant.
|
16
9
|
|
17
|
-
|
18
|
-
@target, @pipe = target, pipe
|
19
|
-
end
|
10
|
+
step = Wrap.for(wrap, pipe)
|
20
11
|
|
21
|
-
|
22
|
-
|
23
|
-
end
|
12
|
+
[ step, {} ]
|
13
|
+
end
|
24
14
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
alias_method :step, :| # DISCUSS: uhm...
|
15
|
+
module Wrap
|
16
|
+
def self.for(wrap, pipe)
|
17
|
+
->(input, options) { wrap.(options, input, pipe, & ->{ pipe.(input, options) }) }
|
29
18
|
end
|
30
19
|
end # Wrap
|
31
|
-
|
32
|
-
DSL.macro!(:Wrap, Wrap)
|
33
20
|
end
|
34
21
|
|
35
22
|
# (options, *) => (options, operation, bla)
|
data/lib/trailblazer/version.rb
CHANGED
data/test/docs/dry_test.rb
CHANGED
@@ -19,10 +19,10 @@ class DryContainerTest < Minitest::Spec
|
|
19
19
|
#- dependency injection
|
20
20
|
#- with Dry-container
|
21
21
|
class Create < Trailblazer::Operation
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
22
|
+
step Model( Song, :new )
|
23
|
+
step Contract::Build()
|
24
|
+
step Contract::Validate()
|
25
|
+
step Contract::Persist( method: :sync )
|
26
26
|
end
|
27
27
|
#:key end
|
28
28
|
|
@@ -0,0 +1,164 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class DocsFailFastOptionTest < Minitest::Spec
|
4
|
+
Song = Struct.new(:id, :title) do
|
5
|
+
def self.find_by(id); nil end
|
6
|
+
end
|
7
|
+
|
8
|
+
class MyContract < Reform::Form
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
#:ffopt
|
13
|
+
class Update < Trailblazer::Operation
|
14
|
+
step Model( Song, :find_by )
|
15
|
+
failure :abort!, fail_fast: true
|
16
|
+
step Contract::Build( constant: MyContract )
|
17
|
+
step Contract::Validate( )
|
18
|
+
failure :handle_invalid_contract! # won't be executed if #abort! is executed.
|
19
|
+
|
20
|
+
def abort!(options, params:, **)
|
21
|
+
options["result.model.song"] = "Something went wrong with ID #{params[:id]}!"
|
22
|
+
end
|
23
|
+
# ..
|
24
|
+
end
|
25
|
+
#:ffopt end
|
26
|
+
|
27
|
+
it { Update.(id: 1).inspect("result.model.song", "contract.default").must_equal %{<Result:false [\"Something went wrong with ID 1!\", nil] >} }
|
28
|
+
it do
|
29
|
+
#:ffopt-res
|
30
|
+
result = Update.(id: 1)
|
31
|
+
result["result.model.song"] #=> "Something went wrong with ID 1!"
|
32
|
+
#:ffopt-res end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class DocsFailFastOptionWithStepTest < Minitest::Spec
|
37
|
+
Song = Class.new do
|
38
|
+
def self.find_by(*); Object end
|
39
|
+
end
|
40
|
+
|
41
|
+
#:ffopt-step
|
42
|
+
class Update < Trailblazer::Operation
|
43
|
+
step :empty_id?, fail_fast: true
|
44
|
+
step Model( Song, :find_by )
|
45
|
+
failure :handle_empty_db! # won't be executed if #empty_id? returns falsey.
|
46
|
+
|
47
|
+
def empty_id?(options, params:, **)
|
48
|
+
params[:id] # returns false if :id missing.
|
49
|
+
end
|
50
|
+
end
|
51
|
+
#:ffopt-step end
|
52
|
+
|
53
|
+
it { Update.({ id: nil }).inspect("model").must_equal %{<Result:false [nil] >} }
|
54
|
+
it { Update.({ id: 1 }).inspect("model").must_equal %{<Result:true [Object] >} }
|
55
|
+
it do
|
56
|
+
#:ffopt-step-res
|
57
|
+
result = Update.({ id: nil })
|
58
|
+
|
59
|
+
result.failure? #=> true
|
60
|
+
result["model"] #=> nil
|
61
|
+
#:ffopt-step-res end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class DocsPassFastWithStepOptionTest < Minitest::Spec
|
66
|
+
Song = Struct.new(:id, :title) do
|
67
|
+
def self.find_by(id); nil end
|
68
|
+
end
|
69
|
+
|
70
|
+
class MyContract < Reform::Form
|
71
|
+
end
|
72
|
+
|
73
|
+
#:pfopt-step
|
74
|
+
class Update < Trailblazer::Operation
|
75
|
+
step Model( Song, :find_by )
|
76
|
+
failure :abort!, fail_fast: true
|
77
|
+
step Contract::Build( constant: MyContract )
|
78
|
+
step Contract::Validate( )
|
79
|
+
failure :handle_invalid_contract! # won't be executed if #abort! is executed.
|
80
|
+
|
81
|
+
def abort!(options, params:, **)
|
82
|
+
options["result.model.song"] = "Something went wrong with ID #{params[:id]}!"
|
83
|
+
end
|
84
|
+
# ..
|
85
|
+
end
|
86
|
+
#:pfopt-step end
|
87
|
+
|
88
|
+
it { Update.(id: 1).inspect("result.model.song", "contract.default").must_equal %{<Result:false [\"Something went wrong with ID 1!\", nil] >} }
|
89
|
+
it do
|
90
|
+
#:pfopt-step-res
|
91
|
+
result = Update.(id: 1)
|
92
|
+
result["result.model.song"] #=> "Something went wrong with ID 1!"
|
93
|
+
#:pfopt-step-res end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class DocsFailFastMethodTest < Minitest::Spec
|
98
|
+
Song = Struct.new(:id, :title) do
|
99
|
+
def self.find_by(id); nil end
|
100
|
+
end
|
101
|
+
|
102
|
+
#:ffmeth
|
103
|
+
class Update < Trailblazer::Operation
|
104
|
+
step :filter_params! # emits fail_fast!
|
105
|
+
step Model( Song, :find_by )
|
106
|
+
failure :handle_fail!
|
107
|
+
|
108
|
+
def filter_params!(options, params:, **)
|
109
|
+
unless params[:id]
|
110
|
+
options["result.params"] = "No ID in params!"
|
111
|
+
return Railway.fail_fast!
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def handle_fail!(options, **)
|
116
|
+
options["my.status"] = "Broken!"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
#:ffmeth end
|
120
|
+
|
121
|
+
it { Update.({}).inspect("result.params", "my.status").must_equal %{<Result:false [\"No ID in params!\", nil] >} }
|
122
|
+
it do
|
123
|
+
#:ffmeth-res
|
124
|
+
result = Update.(id: 1)
|
125
|
+
result["result.params"] #=> "No ID in params!"
|
126
|
+
result["my.status"] #=> nil
|
127
|
+
#:ffmeth-res end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
class DocsPassFastMethodTest < Minitest::Spec
|
132
|
+
Song = Struct.new(:id, :title) do
|
133
|
+
def save; end
|
134
|
+
end
|
135
|
+
|
136
|
+
class MyContract < Reform::Form
|
137
|
+
end
|
138
|
+
|
139
|
+
#:pfmeth
|
140
|
+
class Create < Trailblazer::Operation
|
141
|
+
step Model( Song, :new )
|
142
|
+
step :empty_model! # emits pass_fast!
|
143
|
+
step Contract::Build( constant: MyContract )
|
144
|
+
# ..
|
145
|
+
|
146
|
+
def empty_model!(options, is_empty:, model:, **)
|
147
|
+
return unless is_empty
|
148
|
+
model.save
|
149
|
+
Railway.pass_fast!
|
150
|
+
end
|
151
|
+
end
|
152
|
+
#:pfmeth end
|
153
|
+
|
154
|
+
it { Create.({ title: "Tyrant"}, "is_empty" => true).inspect("model").must_equal %{<Result:true [#<struct DocsPassFastMethodTest::Song id=nil, title=nil>] >} }
|
155
|
+
it do
|
156
|
+
#:pfmeth-res
|
157
|
+
result = Create.({}, "is_empty" => true)
|
158
|
+
result["model"] #=> #<Song id=nil, title=nil>
|
159
|
+
#:pfmeth-res end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# fail!
|
164
|
+
# pass!
|