trailblazer 2.0.7 → 2.1.0.beta1
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 +35 -1
- data/Gemfile +6 -12
- data/README.md +3 -1
- data/Rakefile +6 -17
- data/lib/trailblazer.rb +7 -4
- data/lib/trailblazer/deprecation/call.rb +46 -0
- data/lib/trailblazer/deprecation/context.rb +43 -0
- data/lib/trailblazer/operation/contract.rb +40 -9
- data/lib/trailblazer/operation/deprecations.rb +21 -0
- data/lib/trailblazer/operation/guard.rb +5 -5
- data/lib/trailblazer/operation/model.rb +15 -10
- data/lib/trailblazer/operation/nested.rb +56 -85
- data/lib/trailblazer/operation/persist.rb +4 -2
- data/lib/trailblazer/operation/policy.rb +16 -7
- data/lib/trailblazer/operation/pundit.rb +3 -3
- data/lib/trailblazer/operation/representer.rb +5 -0
- data/lib/trailblazer/operation/rescue.rb +12 -9
- data/lib/trailblazer/operation/validate.rb +36 -29
- data/lib/trailblazer/operation/wrap.rb +49 -11
- data/lib/trailblazer/task.rb +20 -0
- data/lib/trailblazer/version.rb +1 -1
- data/test/benchmark.rb +63 -0
- data/test/deprecation/call_test.rb +42 -0
- data/test/deprecation/context_test.rb +19 -0
- data/test/docs/contract_test.rb +73 -53
- data/test/docs/dry_test.rb +2 -2
- data/test/docs/fast_test.rb +133 -13
- data/test/docs/guard_test.rb +28 -35
- data/test/docs/macro_test.rb +1 -1
- data/test/docs/model_test.rb +13 -13
- data/test/docs/nested_test.rb +54 -122
- data/test/docs/operation_test.rb +42 -43
- data/test/docs/pundit_test.rb +16 -16
- data/test/docs/representer_test.rb +18 -18
- data/test/docs/rescue_test.rb +29 -29
- data/test/docs/trace_test.rb +82 -0
- data/test/docs/wrap_test.rb +59 -26
- data/test/module_test.rb +75 -75
- data/test/nested_test.rb +293 -0
- data/test/operation/contract_test.rb +23 -153
- data/test/operation/dsl/contract_test.rb +9 -9
- data/test/operation/dsl/representer_test.rb +169 -169
- data/test/operation/model_test.rb +15 -21
- data/test/operation/persist_test.rb +18 -11
- data/test/operation/pundit_test.rb +25 -23
- data/test/operation/representer_test.rb +254 -254
- data/test/test_helper.rb +5 -2
- data/test/variables_test.rb +158 -0
- data/trailblazer.gemspec +1 -1
- data/untitled +33 -0
- metadata +25 -27
- data/lib/trailblazer/operation/callback.rb +0 -35
- data/lib/trailblazer/operation/procedural/contract.rb +0 -15
- data/lib/trailblazer/operation/procedural/validate.rb +0 -22
- data/test/operation/callback_test.rb +0 -70
- data/test/operation/dsl/callback_test.rb +0 -106
- data/test/operation/params_test.rb +0 -36
- data/test/operation/pipedream_test.rb +0 -59
- data/test/operation/pipetree_test.rb +0 -104
- data/test/operation/present_test.rb +0 -24
- data/test/operation/resolver_test.rb +0 -47
- data/test/operation_test.rb +0 -143
@@ -2,9 +2,11 @@ class Trailblazer::Operation
|
|
2
2
|
module Contract
|
3
3
|
def self.Persist(method: :save, name: "default")
|
4
4
|
path = "contract.#{name}"
|
5
|
-
step = ->(
|
5
|
+
step = ->(options, **) { options[path].send(method) }
|
6
6
|
|
7
|
-
|
7
|
+
task = Railway::TaskBuilder.( step )
|
8
|
+
|
9
|
+
{ task: task, id: "persist.save" }
|
8
10
|
end
|
9
11
|
end
|
10
12
|
end
|
@@ -8,15 +8,19 @@ class Trailblazer::Operation
|
|
8
8
|
@path = path
|
9
9
|
end
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
# incoming low-level {Task API}.
|
12
|
+
# outgoing Task::Binary API.
|
13
|
+
def call((options, flow_options), **circuit_options)
|
14
|
+
condition = options[ @path ] # this allows dependency injection.
|
15
|
+
result = condition.( [options, flow_options], **circuit_options )
|
14
16
|
|
15
17
|
options["policy.#{@name}"] = result["policy"] # assign the policy as a skill.
|
16
18
|
options["result.policy.#{@name}"] = result
|
17
19
|
|
18
20
|
# flow control
|
19
|
-
result.success? # since we & this, it's only executed OnRight and the return boolean decides the direction, input is passed straight through.
|
21
|
+
signal = result.success? ? Trailblazer::Activity::Right : Trailblazer::Activity::Left # since we & this, it's only executed OnRight and the return boolean decides the direction, input is passed straight through.
|
22
|
+
|
23
|
+
return signal, [ options, flow_options ]
|
20
24
|
end
|
21
25
|
end
|
22
26
|
|
@@ -26,10 +30,15 @@ class Trailblazer::Operation
|
|
26
30
|
name = options[:name]
|
27
31
|
path = "policy.#{name}.eval"
|
28
32
|
|
29
|
-
|
30
|
-
|
33
|
+
task = Eval.new( name: name, path: path )
|
34
|
+
|
35
|
+
runner_options = {
|
36
|
+
merge: Trailblazer::Operation::Wrap::Inject::Defaults(
|
37
|
+
path => condition
|
38
|
+
)
|
39
|
+
}
|
31
40
|
|
32
|
-
|
41
|
+
{ task: task, id: path, runner_options: runner_options }
|
33
42
|
end
|
34
43
|
end
|
35
44
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
class Trailblazer::Operation
|
2
2
|
module Policy
|
3
3
|
def self.Pundit(policy_class, action, name: :default)
|
4
|
-
Policy.step(Pundit.build(policy_class, action), name: name)
|
4
|
+
Policy.step( Pundit.build(policy_class, action), name: name )
|
5
5
|
end
|
6
6
|
|
7
7
|
module Pundit
|
@@ -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(
|
19
|
+
def call((options), *)
|
20
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
25
|
def build_policy(options)
|
26
|
-
@policy_class.new(options[
|
26
|
+
@policy_class.new(options[:current_user], options[:model])
|
27
27
|
end
|
28
28
|
|
29
29
|
def result!(success, policy)
|
@@ -12,6 +12,11 @@ class Trailblazer::Operation
|
|
12
12
|
end
|
13
13
|
|
14
14
|
module DSL
|
15
|
+
def self.extended(extender)
|
16
|
+
extender.extend(ClassDependencies)
|
17
|
+
warn "[Trailblazer] Using `representer do...end` is deprecated. Please use a dedicated representer class."
|
18
|
+
end
|
19
|
+
|
15
20
|
def representer(name=:default, constant=nil, &block)
|
16
21
|
heritage.record(:representer, name, constant, &block)
|
17
22
|
|
@@ -1,21 +1,24 @@
|
|
1
1
|
class Trailblazer::Operation
|
2
|
-
|
2
|
+
NoopHandler = lambda { |*| }
|
3
|
+
|
4
|
+
def self.Rescue(*exceptions, handler: NoopHandler, &block)
|
3
5
|
exceptions = [StandardError] unless exceptions.any?
|
4
|
-
handler = Option
|
6
|
+
handler = Trailblazer::Option(handler)
|
5
7
|
|
6
|
-
|
8
|
+
# This block is evaluated by {Wrap} which currently expects a binary return type.
|
9
|
+
rescue_block = ->(options, flow_options, **circuit_options, &nested_activity) {
|
7
10
|
begin
|
8
|
-
|
9
|
-
res.first == ::Pipetree::Railway::Right # FIXME.
|
11
|
+
nested_activity.call
|
10
12
|
rescue *exceptions => exception
|
11
|
-
|
13
|
+
# DISCUSS: should we deprecate this signature and rather apply the Task API here?
|
14
|
+
handler.call(exception, options, **circuit_options) # FIXME: when there's an error here, it shows the wrong exception!
|
12
15
|
false
|
13
16
|
end
|
14
17
|
}
|
15
18
|
|
16
|
-
|
17
|
-
|
18
|
-
[ step, name: "Rescue:#{block.source_location.last}" ]
|
19
|
+
Wrap(rescue_block, id: "Rescue(#{rand(100)})", &block)
|
20
|
+
# FIXME: name
|
21
|
+
# [ step, name: "Rescue:#{block.source_location.last}" ]
|
19
22
|
end
|
20
23
|
end
|
21
24
|
|
@@ -1,58 +1,65 @@
|
|
1
1
|
class Trailblazer::Operation
|
2
2
|
module Contract
|
3
|
-
Railway = Pipetree::Railway
|
4
|
-
|
5
3
|
# result.contract = {..}
|
6
4
|
# result.contract.errors = {..}
|
7
5
|
# Deviate to left track if optional key is not found in params.
|
8
6
|
# 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?
|
7
|
+
def self.Validate(skip_extract: false, name: "default", representer: false, key: nil) # DISCUSS: should we introduce something like Validate::Deserializer?
|
10
8
|
params_path = "contract.#{name}.params" # extract_params! save extracted params here.
|
11
9
|
|
12
|
-
|
10
|
+
extract = Validate::Extract.new( key: key, params_path: params_path ).freeze
|
11
|
+
validate = Validate.new( name: name, representer: representer, params_path: params_path ).freeze
|
13
12
|
|
14
|
-
|
15
|
-
|
13
|
+
# Return the Validate::Call task if the first step, the params extraction, is not desired.
|
14
|
+
if skip_extract || representer
|
15
|
+
return { task: Trailblazer::Activity::Task::Binary( validate ), id: "contract.#{name}.call" }
|
16
|
+
end
|
16
17
|
|
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
18
|
|
21
|
-
|
19
|
+
# Build a simple Railway {Activity} for the internal flow.
|
20
|
+
activity = Trailblazer::Activity::Railway.build do # FIXME: make Activity.build(builder: Railway) do end an <Activity>
|
21
|
+
step Trailblazer::Activity::Task::Binary( extract ), id: "#{params_path}_extract"
|
22
|
+
step Trailblazer::Activity::Task::Binary( validate ), id: "contract.#{name}.call"
|
23
|
+
end
|
22
24
|
|
23
|
-
|
25
|
+
# DISCUSS: use Nested here?
|
26
|
+
# Nested.operation_class.Nested( activity, id: "contract.#{name}.validate" )
|
27
|
+
{ task: activity, id: "contract.#{name}.validate", plus_poles: Trailblazer::Activity::Magnetic::DSL::PlusPoles.from_outputs(activity.outputs) }
|
24
28
|
end
|
25
29
|
|
26
|
-
|
27
|
-
#
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
options[params_path] = key ? options["params"][key] : options["params"]
|
30
|
+
class Validate
|
31
|
+
# Task: extract the contract's input from params by reading `:key`.
|
32
|
+
class Extract
|
33
|
+
def initialize(key:nil, params_path:nil)
|
34
|
+
@key, @params_path = key, params_path
|
32
35
|
end
|
33
36
|
|
34
|
-
|
37
|
+
def call( (options, flow_options), **circuit_options )
|
38
|
+
options[@params_path] = @key ? options[:params][@key] : options[:params]
|
39
|
+
end
|
35
40
|
end
|
36
41
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
validate!(options, name: name, representer: options["representer.#{name}.class"], params_path: params_path)
|
41
|
-
}
|
42
|
-
|
43
|
-
step = Pipetree::Step.new( step, "representer.#{name}.class" => representer )
|
42
|
+
def initialize(name:"default", representer:false, params_path:nil)
|
43
|
+
@name, @representer, @params_path = name, representer, params_path
|
44
|
+
end
|
44
45
|
|
45
|
-
|
46
|
+
# Task: Validates contract `:name`.
|
47
|
+
def call( (options, flow_options), **circuit_options )
|
48
|
+
validate!(
|
49
|
+
options,
|
50
|
+
representer: options["representer.#{@name}.class"] ||= @representer, # FIXME: maybe @representer should use DI.
|
51
|
+
params_path: @params_path
|
52
|
+
)
|
46
53
|
end
|
47
54
|
|
48
|
-
def
|
49
|
-
path = "contract.#{name}"
|
55
|
+
def validate!(options, representer:false, from: :document, params_path:nil)
|
56
|
+
path = "contract.#{@name}"
|
50
57
|
contract = options[path]
|
51
58
|
|
52
59
|
# this is for 1.1-style compatibility and should be removed once we have Deserializer in place:
|
53
60
|
options["result.#{path}"] = result =
|
54
61
|
if representer
|
55
|
-
# use
|
62
|
+
# use :document as the body and let the representer deserialize to the contract.
|
56
63
|
# this will be simplified once we have Deserializer.
|
57
64
|
# translates to contract.("{document: bla}") { MyRepresenter.new(contract).from_json .. }
|
58
65
|
contract.(options[from]) { |document| representer.new(contract).parse(document) }
|
@@ -1,22 +1,60 @@
|
|
1
1
|
class Trailblazer::Operation
|
2
|
-
Base = self # TODO: we won't need this with 2.1.
|
3
2
|
|
4
|
-
|
5
|
-
operation = Class.new(Base)
|
6
|
-
# DISCUSS: don't instance_exec when |pipe| given?
|
7
|
-
operation.instance_exec(&block) # evaluate the nested pipe.
|
3
|
+
# false is automatically connected to End.failure.
|
8
4
|
|
9
|
-
|
10
|
-
|
5
|
+
def self.Wrap(user_wrap, id: "Wrap/#{rand(100)}", &block)
|
6
|
+
operation_class = Wrap.Operation(block)
|
7
|
+
wrapped = Wrap::Wrapped.new(operation_class, user_wrap)
|
11
8
|
|
12
|
-
|
9
|
+
# connect `false` as an end event, when an exception stopped the wrap, for example.
|
13
10
|
|
14
|
-
|
11
|
+
{ task: wrapped, id: id, plus_poles: Trailblazer::Activity::Magnetic::DSL::PlusPoles.from_outputs(operation_class.outputs) }
|
12
|
+
# TODO: Nested could have a better API and do the "merge" for us!
|
15
13
|
end
|
16
14
|
|
17
15
|
module Wrap
|
18
|
-
def self.
|
19
|
-
|
16
|
+
def self.Operation(block)
|
17
|
+
Class.new( Nested.operation_class, &block ) # Usually resolves to Trailblazer::Operation.
|
18
|
+
end
|
19
|
+
|
20
|
+
# behaves like an operation so it plays with Nested and simply calls the operation in the user-provided block.
|
21
|
+
class Wrapped #< Trailblazer::Operation # FIXME: the inheritance is only to satisfy Nested( Wrapped.new )
|
22
|
+
include Trailblazer::Activity::Interface
|
23
|
+
|
24
|
+
def initialize(operation, user_wrap)
|
25
|
+
@operation = operation
|
26
|
+
@user_wrap = user_wrap
|
27
|
+
end
|
28
|
+
|
29
|
+
def call( (options, flow_options), **circuit_options )
|
30
|
+
block_calling_wrapped = -> {
|
31
|
+
args, circuit_options_with_wrap_static = Railway::TaskWrap.arguments_for_call( @operation, [options, flow_options], **circuit_options )
|
32
|
+
|
33
|
+
# TODO: this is not so nice, still working out how to separate all those bits and pieces.
|
34
|
+
@operation.instance_variable_get(:@process).( args, **circuit_options.merge(circuit_options_with_wrap_static) ) # FIXME: arguments_for_call don't return the full circuit_options, :exec_context gets lost.
|
35
|
+
}
|
36
|
+
|
37
|
+
# call the user's Wrap {} block in the operation.
|
38
|
+
# This will invoke block_calling_wrapped above if the user block yields.
|
39
|
+
returned = @user_wrap.( options, flow_options, **circuit_options, &block_calling_wrapped )
|
40
|
+
|
41
|
+
# returned could be
|
42
|
+
# 1. the 1..>=3 Task interface result
|
43
|
+
# 2. false
|
44
|
+
# 3. true or something else, but not the Task interface (when rescue isn't needed)
|
45
|
+
|
46
|
+
# legacy outcome.
|
47
|
+
# FIXME: we *might* return some "older version" of options here!
|
48
|
+
if returned === false
|
49
|
+
return @operation.outputs[:failure].signal, [options, flow_options]
|
50
|
+
end
|
51
|
+
|
52
|
+
returned # let's hope returned is one of activity's Ends.
|
53
|
+
end
|
54
|
+
|
55
|
+
def outputs
|
56
|
+
@operation.outputs # FIXME: we don't map false, yet
|
57
|
+
end
|
20
58
|
end
|
21
59
|
end # Wrap
|
22
60
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
class Activity
|
3
|
+
module Task
|
4
|
+
# Convenience functions for tasks. Totally optional.
|
5
|
+
|
6
|
+
# Task::Binary aka "step"
|
7
|
+
# Step is binary task: true=> Right, false=>Left.
|
8
|
+
# Step call proc.(options, flow_options)
|
9
|
+
# Step is supposed to run Option::KW, so `step` should be Option::KW.
|
10
|
+
#
|
11
|
+
# Returns task to call the proc with (options, flow_options), omitting `direction`.
|
12
|
+
# When called, the task always returns a direction signal.
|
13
|
+
def self.Binary(step, on_true=Activity::Right, on_false=Activity::Left)
|
14
|
+
->(*args) do # Activity/Task interface.
|
15
|
+
[ step.(*args) ? on_true : on_false, *args ] # <=> Activity/Task interface
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/trailblazer/version.rb
CHANGED
data/test/benchmark.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
require "benchmark/ips"
|
4
|
+
|
5
|
+
|
6
|
+
def positional_vs_decompose
|
7
|
+
arg = [ [1, 2], 3 ]
|
8
|
+
|
9
|
+
def a((one, two), three)
|
10
|
+
one + two + three
|
11
|
+
end
|
12
|
+
|
13
|
+
def b(arr, three)
|
14
|
+
arr[0] + arr[1] + three
|
15
|
+
end
|
16
|
+
|
17
|
+
Benchmark.ips do |x|
|
18
|
+
x.report("decompose") { a(*arg) }
|
19
|
+
x.report("positional") { b(*arg) }
|
20
|
+
x.compare!
|
21
|
+
end
|
22
|
+
|
23
|
+
# decompose 5.646M (± 3.1%) i/s - 28.294M in 5.015759s
|
24
|
+
# positional 5.629M (± 8.0%) i/s - 28.069M in 5.029745s
|
25
|
+
end
|
26
|
+
|
27
|
+
# positional_vs_decompose
|
28
|
+
|
29
|
+
##########################################################
|
30
|
+
|
31
|
+
class Callable
|
32
|
+
def initialize(proc, i)
|
33
|
+
@proc = proc
|
34
|
+
@i = i
|
35
|
+
end
|
36
|
+
|
37
|
+
def call(args)
|
38
|
+
@proc.(@i, args)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def scoped(proc, i)
|
43
|
+
->(args) { proc.(i, args) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def object_vs_method_scope
|
47
|
+
proc = ->(i, args) { i + args }
|
48
|
+
|
49
|
+
callable = Callable.new(proc, 2)
|
50
|
+
scoped = scoped(proc, 2)
|
51
|
+
|
52
|
+
Benchmark.ips do |x|
|
53
|
+
x.report("callable") { callable.(1) }
|
54
|
+
x.report("scoped") { scoped.(1) }
|
55
|
+
x.compare!
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
object_vs_method_scope
|
60
|
+
|
61
|
+
# Comparison:
|
62
|
+
# callable: 4620906.3 i/s
|
63
|
+
# scoped: 3388535.6 i/s - 1.36x slower
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
require "trailblazer/deprecation/context"
|
4
|
+
require "trailblazer/deprecation/call"
|
5
|
+
|
6
|
+
class DeprecationCallTest < Minitest::Spec
|
7
|
+
class Create < Trailblazer::Operation
|
8
|
+
step :create_model
|
9
|
+
|
10
|
+
def create_model(options, params:, **)
|
11
|
+
options["model"] = params.inspect
|
12
|
+
options[:user] = options["current_user"]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it "works with correct style" do
|
17
|
+
result = Create.( params: { title: "Hello" } )
|
18
|
+
result.inspect(:model, :user, :current_user, :params).must_equal %{<Result:false ["{:title=>\\\"Hello\\\"}", nil, nil, {:title=>\"Hello\"}] >}
|
19
|
+
end
|
20
|
+
|
21
|
+
it "works with correct style plus dependencies" do
|
22
|
+
result = Create.( params: { title: "Hello" }, current_user: Object )
|
23
|
+
result.inspect(:model, :user, :current_user, :params).must_equal %{<Result:true ["{:title=>\\\"Hello\\\"}", Object, Object, {:title=>\"Hello\"}] >}
|
24
|
+
end
|
25
|
+
|
26
|
+
it "converts old positional style" do
|
27
|
+
result = Create.( { title: "Hello" }, "current_user" => user=Object )
|
28
|
+
result.inspect(:model, :user, :current_user, :params).must_equal %{<Result:true ["{:title=>\\\"Hello\\\"}", Object, Object, {:title=>\"Hello\"}] >}
|
29
|
+
end
|
30
|
+
|
31
|
+
class WeirdStrongParameters < Hash
|
32
|
+
end
|
33
|
+
|
34
|
+
it "converts old positional style with StrongParameters" do
|
35
|
+
params = WeirdStrongParameters.new
|
36
|
+
params[:title] = "Hello"
|
37
|
+
|
38
|
+
result = Create.( params, "current_user" => user=Object )
|
39
|
+
|
40
|
+
result.inspect(:model, :user, :current_user, :params).must_equal %{<Result:true ["{:title=>\\\"Hello\\\"}", Object, Object, {:title=>\"Hello\"}] >}
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
require "trailblazer/deprecation/context"
|
4
|
+
|
5
|
+
class DeprecationContextTest < Minitest::Spec
|
6
|
+
class Create < Trailblazer::Operation
|
7
|
+
step :create_model
|
8
|
+
|
9
|
+
def create_model(options, params:, **)
|
10
|
+
options["model"] = params.inspect
|
11
|
+
options[:user] = options["current_user"]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
it do
|
16
|
+
result = Create.( "params"=> {title: "Hello"}, "current_user" => user=Object)
|
17
|
+
result.inspect(:model, :user, :current_user, :params).must_equal %{<Result:true ["{:title=>\\\"Hello\\\"}", Object, Object, {:title=>\"Hello\"}] >}
|
18
|
+
end
|
19
|
+
end
|