riak-client 0.9.0.beta → 0.9.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/Gemfile +10 -7
  2. data/Rakefile +21 -3
  3. data/erl_src/riak_kv_test_backend.beam +0 -0
  4. data/erl_src/riak_kv_test_backend.erl +29 -13
  5. data/lib/riak/bucket.rb +1 -1
  6. data/lib/riak/cache_store.rb +1 -1
  7. data/lib/riak/client.rb +119 -8
  8. data/lib/riak/client/beefcake/messages.rb +162 -0
  9. data/lib/riak/client/beefcake/object_methods.rb +92 -0
  10. data/lib/riak/client/beefcake_protobuffs_backend.rb +186 -0
  11. data/lib/riak/client/curb_backend.rb +10 -16
  12. data/lib/riak/client/excon_backend.rb +14 -18
  13. data/lib/riak/client/http_backend.rb +13 -13
  14. data/lib/riak/client/http_backend/object_methods.rb +1 -1
  15. data/lib/riak/client/http_backend/transport_methods.rb +6 -2
  16. data/lib/riak/client/net_http_backend.rb +33 -20
  17. data/lib/riak/client/protobuffs_backend.rb +103 -0
  18. data/lib/riak/client/pump.rb +44 -0
  19. data/lib/riak/failed_request.rb +58 -3
  20. data/lib/riak/locale/en.yml +11 -3
  21. data/lib/riak/map_reduce.rb +15 -6
  22. data/lib/riak/map_reduce/filter_builder.rb +4 -4
  23. data/lib/riak/test_server.rb +5 -1
  24. data/lib/riak/util/multipart.rb +30 -16
  25. data/lib/riak/util/multipart/stream_parser.rb +74 -0
  26. data/riak-client.gemspec +14 -12
  27. data/spec/fixtures/server.cert.crt +15 -0
  28. data/spec/fixtures/server.cert.key +15 -0
  29. data/spec/fixtures/test.pem +1 -0
  30. data/spec/integration/riak/http_backends_spec.rb +45 -0
  31. data/spec/integration/riak/protobuffs_backends_spec.rb +45 -0
  32. data/spec/integration/riak/test_server_spec.rb +2 -2
  33. data/spec/riak/bucket_spec.rb +4 -4
  34. data/spec/riak/client_spec.rb +209 -3
  35. data/spec/riak/excon_backend_spec.rb +8 -7
  36. data/spec/riak/http_backend/configuration_spec.rb +64 -0
  37. data/spec/riak/http_backend/object_methods_spec.rb +1 -1
  38. data/spec/riak/http_backend/transport_methods_spec.rb +129 -0
  39. data/spec/riak/http_backend_spec.rb +13 -1
  40. data/spec/riak/map_reduce/filter_builder_spec.rb +45 -0
  41. data/spec/riak/map_reduce/phase_spec.rb +149 -0
  42. data/spec/riak/map_reduce_spec.rb +5 -5
  43. data/spec/riak/net_http_backend_spec.rb +1 -0
  44. data/spec/riak/{object_spec.rb → robject_spec.rb} +1 -1
  45. data/spec/riak/stream_parser_spec.rb +66 -0
  46. data/spec/support/drb_mock_server.rb +2 -2
  47. data/spec/support/http_backend_implementation_examples.rb +27 -0
  48. data/spec/support/mock_server.rb +22 -1
  49. data/spec/support/unified_backend_examples.rb +255 -0
  50. metadata +43 -54
@@ -23,14 +23,14 @@ else
23
23
 
24
24
  describe Riak::Client::ExconBackend do
25
25
  def setup_http_mock(method, uri, options={})
26
- method = method.to_s.upcase
27
- uri = URI.parse(uri)
28
- path = uri.path || "/"
29
- query = uri.query || ""
30
- status = options[:status] ? Array(options[:status]).first.to_i : 200
31
- body = options[:body] || []
26
+ method = method.to_s.upcase
27
+ uri = URI.parse(uri)
28
+ path = uri.path || "/"
29
+ query = uri.query || ""
30
+ body = options[:body] || []
32
31
  headers = options[:headers] || {}
33
- headers['Content-Type'] ||= "text/plain"
32
+ headers['Content-Type'] ||= "text/plain"
33
+ status = options[:status] ? Array(options[:status]).first.to_i : 200
34
34
  @_mock_set = [status, headers, method, path, query, body]
35
35
  $mock_server.expect(*@_mock_set)
36
36
  end
@@ -73,4 +73,5 @@ else
73
73
  end.should_not raise_error
74
74
  end
75
75
  end
76
+
76
77
  end
@@ -0,0 +1,64 @@
1
+ # Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ require File.expand_path("../../spec_helper", File.dirname(__FILE__))
15
+
16
+ describe Riak::Client::HTTPBackend::Configuration do
17
+ before do
18
+ @client = Riak::Client.new
19
+ @backend = Riak::Client::HTTPBackend.new(@client)
20
+ end
21
+
22
+ it "should memoize the server config" do
23
+ @backend.should_receive(:get).with(200, "/", {}, {}).once.and_return(:headers => {'link' => ['</riak>; rel="riak_kv_wm_link_walker",</mapred>; rel="riak_kv_wm_mapred",</ping>; rel="riak_kv_wm_ping",</riak>; rel="riak_kv_wm_raw",</stats>; rel="riak_kv_wm_stats"']})
24
+ @backend.send(:riak_kv_wm_link_walker).should == "/riak"
25
+ @backend.send(:riak_kv_wm_raw).should == "/riak"
26
+ end
27
+
28
+ {
29
+ :riak_kv_wm_raw => :prefix,
30
+ :riak_kv_wm_link_walker => :prefix,
31
+ :riak_kv_wm_mapred => :mapred
32
+ }.each do |resource, alternate|
33
+ it "should detect the #{resource} resource from the configuration URL" do
34
+ @backend.should_receive(:get).with(200, "/", {}, {}).and_return(:headers => {'link' => [%Q{</path>; rel="#{resource}"}]})
35
+ @backend.send(resource).should == "/path"
36
+ end
37
+ it "should fallback to client.#{alternate} if the #{resource} resource is not found" do
38
+ @backend.should_receive(:get).with(200, "/", {}, {}).and_return(:headers => {'link' => ['</>; rel="top"']})
39
+ @backend.send(resource).should == @client.send(alternate)
40
+ end
41
+ it "should fallback to client.#{alternate} if request fails" do
42
+ @backend.should_receive(:get).with(200, "/", {}, {}).and_raise(Riak::HTTPFailedRequest.new(:get, 200, 404, {}, ""))
43
+ @backend.send(resource).should == @client.send(alternate)
44
+ end
45
+ end
46
+
47
+ {
48
+ :riak_kv_wm_ping => "/ping",
49
+ :riak_kv_wm_stats => "/stats"
50
+ }.each do |resource, default|
51
+ it "should detect the #{resource} resource from the configuration URL" do
52
+ @backend.should_receive(:get).with(200, "/", {}, {}).and_return(:headers => {'link' => [%Q{</path>; rel="#{resource}"}]})
53
+ @backend.send(resource).should == "/path"
54
+ end
55
+ it "should fallback to #{default.inspect} if the #{resource} resource is not found" do
56
+ @backend.should_receive(:get).with(200, "/", {}, {}).and_return(:headers => {'link' => ['</>; rel="top"']})
57
+ @backend.send(resource).should == default
58
+ end
59
+ it "should fallback to #{default.inspect} if request fails" do
60
+ @backend.should_receive(:get).with(200, "/", {}, {}).and_raise(Riak::HTTPFailedRequest.new(:get, 200, 404, {}, ""))
61
+ @backend.send(resource).should == default
62
+ end
63
+ end
64
+ end
@@ -84,7 +84,7 @@ describe Riak::Client::HTTPBackend::ObjectMethods do
84
84
  end
85
85
 
86
86
  it "should parse and escape the location header into the key when present" do
87
- @backend.load_object(@object, {:headers => {"content-type" => ["application/json"], "location" => ["/riak/foo/%5Bbaz%5D"]}})
87
+ @backend.load_object(@object, {:headers => {"content-type" => ["application/json"], "location" => ["/riak/foo/%5Bbaz%5D?vtag=1234"]}})
88
88
  @object.key.should == "[baz]"
89
89
  end
90
90
 
@@ -0,0 +1,129 @@
1
+ # Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License
14
+ require File.expand_path("../../spec_helper", File.dirname(__FILE__))
15
+
16
+ describe Riak::Client::HTTPBackend::TransportMethods do
17
+ before :each do
18
+ @client = Riak::Client.new
19
+ @backend = Riak::Client::HTTPBackend.new(@client)
20
+ @backend.instance_variable_set(:@server_config, {})
21
+ end
22
+
23
+ it "should generate default headers for requests based on the client settings" do
24
+ @client.client_id = "testing"
25
+ @backend.default_headers.should == {"X-Riak-ClientId" => "testing", "Accept" => "multipart/mixed, application/json;q=0.7, */*;q=0.5"}
26
+ end
27
+
28
+ it "should generate a root URI based on the client settings" do
29
+ @backend.root_uri.should be_kind_of(URI)
30
+ @backend.root_uri.to_s.should == "http://127.0.0.1:8098"
31
+ end
32
+
33
+ it "should compute a URI from a relative resource path" do
34
+ @backend.path("baz").should be_kind_of(URI)
35
+ @backend.path("foo").to_s.should == "http://127.0.0.1:8098/foo"
36
+ @backend.path("foo", "bar").to_s.should == "http://127.0.0.1:8098/foo/bar"
37
+ @backend.path("/foo/bar").to_s.should == "http://127.0.0.1:8098/foo/bar"
38
+ end
39
+
40
+ it "should compute a URI from a relative resource path with a hash of query parameters" do
41
+ @backend.path("baz", :r => 2).to_s.should == "http://127.0.0.1:8098/baz?r=2"
42
+ end
43
+
44
+ it "should raise an error if a resource path is too short" do
45
+ lambda { @backend.verify_path!(["/riak/"]) }.should raise_error(ArgumentError)
46
+ lambda { @backend.verify_path!(["/riak/", "foo"]) }.should_not raise_error
47
+ lambda { @backend.verify_path!(["/mapred"]) }.should_not raise_error
48
+ end
49
+
50
+ describe "verify_path_and_body!" do
51
+ it "should separate the path and body from given arguments" do
52
+ uri, data = @backend.verify_path_and_body!(["/riak/", "foo", "This is the body."])
53
+ uri.should == ["/riak/", "foo"]
54
+ data.should == "This is the body."
55
+ end
56
+
57
+ it "should raise an error if the body is not a string or IO" do
58
+ lambda { @backend.verify_path_and_body!(["/riak/", "foo", nil]) }.should raise_error(ArgumentError)
59
+ lambda { @backend.verify_path_and_body!(["/riak/", "foo", File.open("spec/fixtures/cat.jpg")]) }.should_not raise_error(ArgumentError)
60
+ end
61
+
62
+ it "should raise an error if a body is not given" do
63
+ lambda { @backend.verify_path_and_body!(["/riak/", "foo"])}.should raise_error(ArgumentError)
64
+ end
65
+
66
+ it "should raise an error if a path is not given" do
67
+ lambda { @backend.verify_path_and_body!(["/riak/"])}.should raise_error(ArgumentError)
68
+ end
69
+ end
70
+
71
+ describe "detecting valid response codes" do
72
+ it "should accept strings or integers for either argument" do
73
+ @backend.should be_valid_response("300", "300")
74
+ @backend.should be_valid_response(300, "300")
75
+ @backend.should be_valid_response("300", 300)
76
+ end
77
+
78
+ it "should accept an array of strings or integers for the expected code" do
79
+ @backend.should be_valid_response([200,304], "200")
80
+ @backend.should be_valid_response(["200",304], "200")
81
+ @backend.should be_valid_response([200,"304"], "200")
82
+ @backend.should be_valid_response(["200","304"], "200")
83
+ @backend.should be_valid_response([200,304], 200)
84
+ end
85
+
86
+ it "should be false when none of the response codes match" do
87
+ @backend.should_not be_valid_response(200, 404)
88
+ @backend.should_not be_valid_response(["200","304"], 404)
89
+ @backend.should_not be_valid_response([200,304], 404)
90
+ end
91
+ end
92
+
93
+ describe "detecting whether a body should be returned" do
94
+ it "should be false when the method is :head" do
95
+ @backend.should_not be_return_body(:head, 200, false)
96
+ end
97
+
98
+ it "should be false when the response code is 204, 205, or 304" do
99
+ @backend.should_not be_return_body(:get, 204, false)
100
+ @backend.should_not be_return_body(:get, 205, false)
101
+ @backend.should_not be_return_body(:get, 304, false)
102
+ end
103
+
104
+ it "should be false when a streaming block was passed" do
105
+ @backend.should_not be_return_body(:get, 200, true)
106
+ end
107
+
108
+ it "should be true when the method is not head, a code other than 204, 205, or 304 was given, and there was no streaming block" do
109
+ [:get, :put, :post, :delete].each do |method|
110
+ [100,101,200,201,202,203,206,300,301,302,303,305,307,400,401,
111
+ 402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,
112
+ 500,501,502,503,504,505].each do |code|
113
+ @backend.should be_return_body(method, code, false)
114
+ @backend.should be_return_body(method, code.to_s, false)
115
+ end
116
+ end
117
+ end
118
+ end
119
+
120
+ it "should force subclasses to implement the perform method" do
121
+ lambda { @backend.send(:perform, :get, "/foo", {}, 200) }.should raise_error(NotImplementedError)
122
+ end
123
+
124
+ it "should allow using the https protocol" do
125
+ @client = Riak::Client.new(:protocol => 'https')
126
+ @backend = Riak::Client::HTTPBackend.new(@client)
127
+ @backend.root_uri.to_s.should eq("https://127.0.0.1:8098")
128
+ end
129
+ end
@@ -76,7 +76,7 @@ describe Riak::Client::HTTPBackend do
76
76
  end
77
77
 
78
78
  it "should raise an exception when the response code is not 200 or 304" do
79
- @backend.should_receive(:get).and_raise(Riak::FailedRequest.new(:get, 200, 500, {}, ''))
79
+ @backend.should_receive(:get).and_raise(Riak::HTTPFailedRequest.new(:get, 200, 500, {}, ''))
80
80
  lambda { @backend.reload_object(@object) }.should raise_error(Riak::FailedRequest)
81
81
  end
82
82
 
@@ -227,6 +227,18 @@ describe Riak::Client::HTTPBackend do
227
227
  @backend.stub!(:post).and_return(response)
228
228
  @backend.mapred(@mr).should == response
229
229
  end
230
+
231
+ it "should stream results through the block" do
232
+ data = File.read("spec/fixtures/multipart-mapreduce.txt")
233
+ @backend.should_receive(:post).with(200, "/mapred", {:chunked => true}, @mr.to_json, hash_including("Content-Type" => "application/json")).and_yield(data)
234
+ block = mock
235
+ block.should_receive(:ping).twice.and_return(true)
236
+ @backend.mapred(@mr) do |phase, data|
237
+ block.ping
238
+ phase.should == 0
239
+ data.should have(1).item
240
+ end
241
+ end
230
242
  end
231
243
 
232
244
  context "getting statistics" do
@@ -0,0 +1,45 @@
1
+ # Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ require File.expand_path("../../spec_helper", File.dirname(__FILE__))
15
+
16
+ describe Riak::MapReduce::FilterBuilder do
17
+ subject { Riak::MapReduce::FilterBuilder.new }
18
+ it "should evaluate the passed block on initialization" do
19
+ subject.class.new do
20
+ matches "foo"
21
+ end.to_a.should == [[:matches, "foo"]]
22
+ end
23
+
24
+ it "should add filters to the list" do
25
+ subject.to_lower
26
+ subject.similar_to("ripple", 3)
27
+ subject.to_a.should == [[:to_lower],[:similar_to, "ripple", 3]]
28
+ end
29
+
30
+ it "should add a logical operation with a block" do
31
+ subject.OR do
32
+ starts_with "foo"
33
+ ends_with "bar"
34
+ end
35
+ subject.to_a.should == [[:or, [[:starts_with, "foo"],[:ends_with, "bar"]]]]
36
+ end
37
+
38
+ it "should raise an error on a filter arity mismatch" do
39
+ lambda { subject.less_than }.should raise_error(ArgumentError)
40
+ end
41
+
42
+ it "should raise an error when a block is not given to a logical operation" do
43
+ lambda { subject._or }.should raise_error(ArgumentError)
44
+ end
45
+ end
@@ -0,0 +1,149 @@
1
+ # Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ require File.expand_path("../../spec_helper", File.dirname(__FILE__))
15
+
16
+ describe Riak::MapReduce::Phase do
17
+ before :each do
18
+ @fun = "function(v,_,_){ return v['values'][0]['data']; }"
19
+ end
20
+
21
+ it "should initialize with a type and a function" do
22
+ phase = Riak::MapReduce::Phase.new(:type => :map, :function => @fun, :language => "javascript")
23
+ phase.type.should == :map
24
+ phase.function.should == @fun
25
+ phase.language.should == "javascript"
26
+ end
27
+
28
+ it "should initialize with a type and an MF" do
29
+ phase = Riak::MapReduce::Phase.new(:type => :map, :function => ["module", "function"], :language => "erlang")
30
+ phase.type.should == :map
31
+ phase.function.should == ["module", "function"]
32
+ phase.language.should == "erlang"
33
+ end
34
+
35
+ it "should initialize with a type and a bucket/key" do
36
+ phase = Riak::MapReduce::Phase.new(:type => :map, :function => {:bucket => "funs", :key => "awesome_map"}, :language => "javascript")
37
+ phase.type.should == :map
38
+ phase.function.should == {:bucket => "funs", :key => "awesome_map"}
39
+ phase.language.should == "javascript"
40
+ end
41
+
42
+ it "should assume the language is erlang when the function is an array" do
43
+ phase = Riak::MapReduce::Phase.new(:type => :map, :function => ["module", "function"])
44
+ phase.language.should == "erlang"
45
+ end
46
+
47
+ it "should assume the language is javascript when the function is a string" do
48
+ phase = Riak::MapReduce::Phase.new(:type => :map, :function => @fun)
49
+ phase.language.should == "javascript"
50
+ end
51
+
52
+ it "should assume the language is javascript when the function is a hash" do
53
+ phase = Riak::MapReduce::Phase.new(:type => :map, :function => {:bucket => "jobs", :key => "awesome_map"})
54
+ phase.language.should == "javascript"
55
+ end
56
+
57
+ it "should accept a WalkSpec for the function when a link phase" do
58
+ phase = Riak::MapReduce::Phase.new(:type => :link, :function => Riak::WalkSpec.new({}))
59
+ phase.function.should be_kind_of(Riak::WalkSpec)
60
+ end
61
+
62
+ it "should raise an error if a WalkSpec is given for a phase type other than :link" do
63
+ lambda { Riak::MapReduce::Phase.new(:type => :map, :function => Riak::WalkSpec.new({})) }.should raise_error(ArgumentError)
64
+ end
65
+
66
+ describe "converting to JSON for the job" do
67
+ before :each do
68
+ @phase = Riak::MapReduce::Phase.new(:type => :map, :function => "")
69
+ end
70
+
71
+ [:map, :reduce].each do |type|
72
+ describe "when a #{type} phase" do
73
+ before :each do
74
+ @phase.type = type
75
+ end
76
+
77
+ it "should be an object with a single key of '#{type}'" do
78
+ @phase.to_json.should =~ /^\{"#{type}":/
79
+ end
80
+
81
+ it "should include the language" do
82
+ @phase.to_json.should =~ /"language":/
83
+ end
84
+
85
+ it "should include the keep value" do
86
+ @phase.to_json.should =~ /"keep":false/
87
+ @phase.keep = true
88
+ @phase.to_json.should =~ /"keep":true/
89
+ end
90
+
91
+ it "should include the function source when the function is a source string" do
92
+ @phase.function = "function(v,_,_){ return v; }"
93
+ @phase.to_json.should include(@phase.function)
94
+ @phase.to_json.should =~ /"source":/
95
+ end
96
+
97
+ it "should include the function name when the function is not a lambda" do
98
+ @phase.function = "Riak.mapValues"
99
+ @phase.to_json.should include('"name":"Riak.mapValues"')
100
+ @phase.to_json.should_not include('"source"')
101
+ end
102
+
103
+ it "should include the bucket and key when referring to a stored function" do
104
+ @phase.function = {:bucket => "design", :key => "wordcount_map"}
105
+ @phase.to_json.should include('"bucket":"design"')
106
+ @phase.to_json.should include('"key":"wordcount_map"')
107
+ end
108
+
109
+ it "should include the module and function when invoking an Erlang function" do
110
+ @phase.function = ["riak_mapreduce", "mapreduce_fun"]
111
+ @phase.to_json.should include('"module":"riak_mapreduce"')
112
+ @phase.to_json.should include('"function":"mapreduce_fun"')
113
+ end
114
+ end
115
+ end
116
+
117
+ describe "when a link phase" do
118
+ before :each do
119
+ @phase.type = :link
120
+ @phase.function = {}
121
+ end
122
+
123
+ it "should be an object of a single key 'link'" do
124
+ @phase.to_json.should =~ /^\{"link":/
125
+ end
126
+
127
+ it "should include the bucket" do
128
+ @phase.to_json.should =~ /"bucket":"_"/
129
+ @phase.function[:bucket] = "foo"
130
+ @phase.to_json.should =~ /"bucket":"foo"/
131
+ end
132
+
133
+ it "should include the tag" do
134
+ @phase.to_json.should =~ /"tag":"_"/
135
+ @phase.function[:tag] = "parent"
136
+ @phase.to_json.should =~ /"tag":"parent"/
137
+ end
138
+
139
+ it "should include the keep value" do
140
+ @phase.to_json.should =~ /"keep":false/
141
+ @phase.keep = true
142
+ @phase.to_json.should =~ /"keep":true/
143
+ @phase.keep = false
144
+ @phase.function[:keep] = true
145
+ @phase.to_json.should =~ /"keep":true/
146
+ end
147
+ end
148
+ end
149
+ end