parse-ruby-client 0.1.15 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +1 -0
- data/Gemfile +5 -7
- data/Gemfile.lock +39 -15
- data/README.md +6 -8
- data/VERSION +1 -1
- data/features.md +1 -1
- data/fixtures/vcr_cassettes/test_batch_update_nils_delete_keys.yml +239 -0
- data/fixtures/vcr_cassettes/test_saving_nested_objects.yml +62 -0
- data/fixtures/vcr_cassettes/test_xget.yml +182 -0
- data/lib/parse-ruby-client.rb +7 -2
- data/lib/parse/batch.rb +4 -3
- data/lib/parse/client.rb +26 -18
- data/lib/parse/datatypes.rb +22 -15
- data/lib/parse/http_client.rb +84 -0
- data/lib/parse/object.rb +56 -66
- data/lib/parse/query.rb +11 -1
- data/lib/parse/util.rb +4 -4
- data/parse-ruby-client.gemspec +9 -6
- data/test/test_batch.rb +20 -0
- data/test/test_datatypes.rb +5 -6
- data/test/test_object.rb +9 -14
- data/test/test_query.rb +27 -3
- metadata +11 -8
- data/fixtures/vcr_cassettes/test_circular_save.yml +0 -121
@@ -0,0 +1,84 @@
|
|
1
|
+
module Parse
|
2
|
+
class HttpClient
|
3
|
+
class TimeoutError < StandardError; end
|
4
|
+
|
5
|
+
attr_accessor :base_url, :headers
|
6
|
+
|
7
|
+
def initialize(base_url=nil, headers = {})
|
8
|
+
@base_url = base_url
|
9
|
+
@headers = headers
|
10
|
+
end
|
11
|
+
|
12
|
+
def build_query(hash)
|
13
|
+
hash.to_a.map{|k, v| "#{k}=#{v}"}.join('&')
|
14
|
+
end
|
15
|
+
|
16
|
+
def request(method, uri, headers, options)
|
17
|
+
NotImplementedError.new("Subclass responsibility")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class NetHttpClient < HttpClient
|
22
|
+
class NetHttpResponseWrapper
|
23
|
+
def initialize(response) @response = response end
|
24
|
+
def status() @response.code.to_i end
|
25
|
+
def body() @response.read_body end
|
26
|
+
end
|
27
|
+
|
28
|
+
def request(method, uri, headers, options)
|
29
|
+
request_class = eval("Net::HTTP::#{method.to_s.capitalize}")
|
30
|
+
uri = "#{uri}?#{options[:query]}" if options[:query]
|
31
|
+
request = request_class.new(uri, @headers.dup.update(headers))
|
32
|
+
request.body = options[:data] if options.has_key?(:data)
|
33
|
+
NetHttpResponseWrapper.new(
|
34
|
+
@client.start do
|
35
|
+
@client.request(request)
|
36
|
+
end
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
def base_url=(url)
|
41
|
+
@base_url = url
|
42
|
+
@client = Net::HTTP.new(@base_url.sub('https://', ''), 443)
|
43
|
+
@client.use_ssl = true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class PatronHttpClient < HttpClient
|
48
|
+
def initialize(base_url=nil, headers = {})
|
49
|
+
super
|
50
|
+
@session = Patron::Session.new
|
51
|
+
@session.timeout = 30
|
52
|
+
@session.connect_timeout = 30
|
53
|
+
@session.headers.update(@headers)
|
54
|
+
end
|
55
|
+
|
56
|
+
def build_query(hash)
|
57
|
+
Patron::Util.build_query_pairs_from_hash(hash).join('&')
|
58
|
+
end
|
59
|
+
|
60
|
+
def request(method, uri, headers, options)
|
61
|
+
@session.request(method, uri, headers, options)
|
62
|
+
rescue Patron::TimeoutError => e
|
63
|
+
raise HttpClient::TimeoutError.new(e)
|
64
|
+
end
|
65
|
+
|
66
|
+
def base_url
|
67
|
+
@session.base_url
|
68
|
+
end
|
69
|
+
|
70
|
+
def base_url=(url)
|
71
|
+
@session.base_url = url
|
72
|
+
end
|
73
|
+
|
74
|
+
def headers
|
75
|
+
@session.headers
|
76
|
+
end
|
77
|
+
|
78
|
+
def headers=(hash)
|
79
|
+
@session.headers = hash
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
DEFAULT_HTTP_CLIENT = defined?(JRUBY_VERSION) ? NetHttpClient : PatronHttpClient
|
84
|
+
end
|
data/lib/parse/object.rb
CHANGED
@@ -35,7 +35,7 @@ module Parse
|
|
35
35
|
end
|
36
36
|
|
37
37
|
def pointer
|
38
|
-
Parse::Pointer.new(
|
38
|
+
Parse::Pointer.new(rest_api_hash) unless new?
|
39
39
|
end
|
40
40
|
|
41
41
|
# make it easier to deal with the ambiguity of whether you're passed a pointer or object
|
@@ -43,68 +43,10 @@ module Parse
|
|
43
43
|
self
|
44
44
|
end
|
45
45
|
|
46
|
-
# Merge a hash parsed from the JSON representation into
|
47
|
-
# this instance. This will extract the reserved fields,
|
48
|
-
# merge the hash keys, and then ensure that the reserved
|
49
|
-
# fields do not occur in the underlying hash storage.
|
50
|
-
def parse(data)
|
51
|
-
if !data
|
52
|
-
return
|
53
|
-
end
|
54
|
-
|
55
|
-
@parse_object_id ||= data[Protocol::KEY_OBJECT_ID]
|
56
|
-
|
57
|
-
if data.has_key? Protocol::KEY_CREATED_AT
|
58
|
-
@created_at = DateTime.parse data[Protocol::KEY_CREATED_AT]
|
59
|
-
end
|
60
|
-
|
61
|
-
if data.has_key? Protocol::KEY_UPDATED_AT
|
62
|
-
@updated_at = DateTime.parse data[Protocol::KEY_UPDATED_AT]
|
63
|
-
end
|
64
|
-
|
65
|
-
data.each do |k,v|
|
66
|
-
if k.is_a? Symbol
|
67
|
-
k = k.to_s
|
68
|
-
end
|
69
|
-
|
70
|
-
if k != Parse::Protocol::KEY_TYPE
|
71
|
-
self[k] = v
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
self
|
76
|
-
end
|
77
|
-
|
78
46
|
def new?
|
79
47
|
self["objectId"].nil?
|
80
48
|
end
|
81
49
|
|
82
|
-
def safe_hash
|
83
|
-
without_reserved = self.dup
|
84
|
-
Protocol::RESERVED_KEYS.each { |k| without_reserved.delete(k) }
|
85
|
-
|
86
|
-
without_relations = without_reserved
|
87
|
-
without_relations.each do |k,v|
|
88
|
-
if v.is_a? Hash
|
89
|
-
if v[Protocol::KEY_TYPE] == Protocol::TYPE_RELATION
|
90
|
-
without_relations.delete(k)
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
without_relations.each do |k, v|
|
96
|
-
without_relations[k] = Parse.pointerize_value(v)
|
97
|
-
end
|
98
|
-
|
99
|
-
without_relations
|
100
|
-
end
|
101
|
-
|
102
|
-
def safe_json
|
103
|
-
safe_hash.to_json
|
104
|
-
end
|
105
|
-
|
106
|
-
private :parse
|
107
|
-
|
108
50
|
# Write the current state of the local object to the API.
|
109
51
|
# If the object has never been saved before, this will create
|
110
52
|
# a new object, otherwise it will update the existing stored object.
|
@@ -116,7 +58,7 @@ module Parse
|
|
116
58
|
method = :post
|
117
59
|
end
|
118
60
|
|
119
|
-
body =
|
61
|
+
body = safe_hash.to_json
|
120
62
|
data = Parse.client.request(self.uri, method, body)
|
121
63
|
|
122
64
|
if data
|
@@ -133,20 +75,36 @@ module Parse
|
|
133
75
|
self
|
134
76
|
end
|
135
77
|
|
136
|
-
|
78
|
+
# representation of object to send on saves
|
79
|
+
def safe_hash
|
137
80
|
Hash[self.map do |key, value|
|
138
|
-
|
139
|
-
|
81
|
+
if Protocol::RESERVED_KEYS.include?(key)
|
82
|
+
nil
|
83
|
+
elsif value.is_a?(Hash) && value[Protocol::KEY_TYPE] == Protocol::TYPE_RELATION
|
84
|
+
nil
|
85
|
+
elsif value.nil?
|
86
|
+
[key, Protocol::DELETE_OP]
|
140
87
|
else
|
141
|
-
|
88
|
+
[key, Parse.pointerize_value(value)]
|
142
89
|
end
|
90
|
+
end.compact]
|
91
|
+
end
|
143
92
|
|
144
|
-
|
93
|
+
# full REST api representation of object
|
94
|
+
def rest_api_hash
|
95
|
+
self.merge(Parse::Protocol::KEY_CLASS_NAME => class_name)
|
96
|
+
end
|
97
|
+
|
98
|
+
def to_h(*a)
|
99
|
+
Hash[rest_api_hash.map do |key, value|
|
100
|
+
[key, value.respond_to?(:to_h) ? value.to_h : value]
|
145
101
|
end]
|
146
102
|
end
|
103
|
+
alias :as_json :to_h
|
104
|
+
alias :to_hash :to_h
|
147
105
|
|
148
106
|
def to_json(*a)
|
149
|
-
|
107
|
+
to_h.to_json(*a)
|
150
108
|
end
|
151
109
|
|
152
110
|
def to_s
|
@@ -220,6 +178,38 @@ module Parse
|
|
220
178
|
|
221
179
|
private
|
222
180
|
|
181
|
+
# Merge a hash parsed from the JSON representation into
|
182
|
+
# this instance. This will extract the reserved fields,
|
183
|
+
# merge the hash keys, and then ensure that the reserved
|
184
|
+
# fields do not occur in the underlying hash storage.
|
185
|
+
def parse(data)
|
186
|
+
if !data
|
187
|
+
return
|
188
|
+
end
|
189
|
+
|
190
|
+
@parse_object_id ||= data[Protocol::KEY_OBJECT_ID]
|
191
|
+
|
192
|
+
if data.has_key? Protocol::KEY_CREATED_AT
|
193
|
+
@created_at = DateTime.parse data[Protocol::KEY_CREATED_AT]
|
194
|
+
end
|
195
|
+
|
196
|
+
if data.has_key? Protocol::KEY_UPDATED_AT
|
197
|
+
@updated_at = DateTime.parse data[Protocol::KEY_UPDATED_AT]
|
198
|
+
end
|
199
|
+
|
200
|
+
data.each do |k,v|
|
201
|
+
if k.is_a? Symbol
|
202
|
+
k = k.to_s
|
203
|
+
end
|
204
|
+
|
205
|
+
if k != Parse::Protocol::KEY_TYPE
|
206
|
+
self[k] = v
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
self
|
211
|
+
end
|
212
|
+
|
223
213
|
def array_op(field, operation, value)
|
224
214
|
raise "field #{field} not an array" if self[field] && !self[field].is_a?(Array)
|
225
215
|
|
data/lib/parse/query.rb
CHANGED
@@ -125,7 +125,17 @@ module Parse
|
|
125
125
|
[:count, :limit, :skip, :include].each {|a| merge_attribute(a, query)}
|
126
126
|
Parse.client.logger.info{"Parse query for #{uri} #{CGI.unescape(query.inspect)}"}
|
127
127
|
response = Parse.client.request uri, :get, nil, query
|
128
|
-
|
128
|
+
|
129
|
+
if response.is_a?(Hash) && response.has_key?(Protocol::KEY_RESULTS) && response[Protocol::KEY_RESULTS].is_a?(Array)
|
130
|
+
parsed_results = response[Protocol::KEY_RESULTS].map{|o| Parse.parse_json(class_name, o)}
|
131
|
+
if response.keys.size == 1
|
132
|
+
parsed_results
|
133
|
+
else
|
134
|
+
response.dup.merge(Protocol::KEY_RESULTS => parsed_results)
|
135
|
+
end
|
136
|
+
else
|
137
|
+
raise ParseError.new("query response not a Hash with #{Protocol::KEY_RESULTS} key: #{response.class} #{response.inspect}")
|
138
|
+
end
|
129
139
|
end
|
130
140
|
|
131
141
|
private
|
data/lib/parse/util.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Parse
|
2
2
|
|
3
3
|
# Parse a JSON representation into a fully instantiated
|
4
|
-
# class. obj can be either a
|
4
|
+
# class. obj can be either a primitive or a Hash of primitives as parsed
|
5
5
|
# by JSON.parse
|
6
6
|
# @param class_name [Object]
|
7
7
|
# @param obj [Object]
|
@@ -19,8 +19,6 @@ module Parse
|
|
19
19
|
# If it's a datatype hash
|
20
20
|
if obj.has_key?(Protocol::KEY_TYPE)
|
21
21
|
parse_datatype obj
|
22
|
-
elsif obj.size == 1 && obj.has_key?(Protocol::KEY_RESULTS) && obj[Protocol::KEY_RESULTS].is_a?(Array)
|
23
|
-
obj[Protocol::KEY_RESULTS].collect { |o| parse_json(class_name, o) }
|
24
22
|
elsif class_name # otherwise it must be a regular object, so deep parse it avoiding re-JSON.parsing raw Strings
|
25
23
|
Parse::Object.new class_name, Hash[obj.map{|k,v| [k, parse_json(nil, v)]}]
|
26
24
|
else # plain old hash
|
@@ -54,7 +52,9 @@ module Parse
|
|
54
52
|
|
55
53
|
def Parse.pointerize_value(obj)
|
56
54
|
if obj.kind_of?(Parse::Object)
|
57
|
-
obj.pointer
|
55
|
+
p = obj.pointer
|
56
|
+
raise ArgumentError.new("new object used in context requiring pointer #{obj}") unless p
|
57
|
+
p
|
58
58
|
elsif obj.is_a?(Array)
|
59
59
|
obj.map do |v|
|
60
60
|
Parse.pointerize_value(v)
|
data/parse-ruby-client.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "parse-ruby-client"
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.2.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Alan deLevie", "Adam Alpern"]
|
12
|
-
s.date = "2013-
|
12
|
+
s.date = "2013-10-08"
|
13
13
|
s.description = "A simple Ruby client for the parse.com REST API"
|
14
14
|
s.email = "adelevie@gmail.com"
|
15
15
|
s.extra_rdoc_files = [
|
@@ -34,8 +34,8 @@ Gem::Specification.new do |s|
|
|
34
34
|
"fixtures/vcr_cassettes/test_batch_create_object.yml",
|
35
35
|
"fixtures/vcr_cassettes/test_batch_delete_object.yml",
|
36
36
|
"fixtures/vcr_cassettes/test_batch_run.yml",
|
37
|
+
"fixtures/vcr_cassettes/test_batch_update_nils_delete_keys.yml",
|
37
38
|
"fixtures/vcr_cassettes/test_batch_update_object.yml",
|
38
|
-
"fixtures/vcr_cassettes/test_circular_save.yml",
|
39
39
|
"fixtures/vcr_cassettes/test_created_at.yml",
|
40
40
|
"fixtures/vcr_cassettes/test_decrement.yml",
|
41
41
|
"fixtures/vcr_cassettes/test_deep_parse.yml",
|
@@ -52,18 +52,21 @@ Gem::Specification.new do |s|
|
|
52
52
|
"fixtures/vcr_cassettes/test_pointer.yml",
|
53
53
|
"fixtures/vcr_cassettes/test_save_with_sub_objects.yml",
|
54
54
|
"fixtures/vcr_cassettes/test_saving_boolean_values.yml",
|
55
|
+
"fixtures/vcr_cassettes/test_saving_nested_objects.yml",
|
55
56
|
"fixtures/vcr_cassettes/test_server_update.yml",
|
56
57
|
"fixtures/vcr_cassettes/test_simple_save.yml",
|
57
58
|
"fixtures/vcr_cassettes/test_text_file_save.yml",
|
58
59
|
"fixtures/vcr_cassettes/test_update.yml",
|
59
60
|
"fixtures/vcr_cassettes/test_updated_at.yml",
|
60
61
|
"fixtures/vcr_cassettes/test_user_save.yml",
|
62
|
+
"fixtures/vcr_cassettes/test_xget.yml",
|
61
63
|
"lib/parse-ruby-client.rb",
|
62
64
|
"lib/parse/batch.rb",
|
63
65
|
"lib/parse/client.rb",
|
64
66
|
"lib/parse/cloud.rb",
|
65
67
|
"lib/parse/datatypes.rb",
|
66
68
|
"lib/parse/error.rb",
|
69
|
+
"lib/parse/http_client.rb",
|
67
70
|
"lib/parse/model.rb",
|
68
71
|
"lib/parse/object.rb",
|
69
72
|
"lib/parse/protocol.rb",
|
@@ -106,7 +109,7 @@ Gem::Specification.new do |s|
|
|
106
109
|
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
107
110
|
s.add_development_dependency(%q<test-unit>, ["= 2.5.0"])
|
108
111
|
s.add_development_dependency(%q<mocha>, ["= 0.12.0"])
|
109
|
-
s.add_development_dependency(%q<jeweler>, ["
|
112
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.8.5"])
|
110
113
|
s.add_development_dependency(%q<simplecov>, [">= 0"])
|
111
114
|
s.add_development_dependency(%q<webmock>, [">= 0"])
|
112
115
|
s.add_development_dependency(%q<vcr>, [">= 0"])
|
@@ -117,7 +120,7 @@ Gem::Specification.new do |s|
|
|
117
120
|
s.add_dependency(%q<shoulda>, [">= 0"])
|
118
121
|
s.add_dependency(%q<test-unit>, ["= 2.5.0"])
|
119
122
|
s.add_dependency(%q<mocha>, ["= 0.12.0"])
|
120
|
-
s.add_dependency(%q<jeweler>, ["
|
123
|
+
s.add_dependency(%q<jeweler>, ["~> 1.8.5"])
|
121
124
|
s.add_dependency(%q<simplecov>, [">= 0"])
|
122
125
|
s.add_dependency(%q<webmock>, [">= 0"])
|
123
126
|
s.add_dependency(%q<vcr>, [">= 0"])
|
@@ -129,7 +132,7 @@ Gem::Specification.new do |s|
|
|
129
132
|
s.add_dependency(%q<shoulda>, [">= 0"])
|
130
133
|
s.add_dependency(%q<test-unit>, ["= 2.5.0"])
|
131
134
|
s.add_dependency(%q<mocha>, ["= 0.12.0"])
|
132
|
-
s.add_dependency(%q<jeweler>, ["
|
135
|
+
s.add_dependency(%q<jeweler>, ["~> 1.8.5"])
|
133
136
|
s.add_dependency(%q<simplecov>, [">= 0"])
|
134
137
|
s.add_dependency(%q<webmock>, [">= 0"])
|
135
138
|
s.add_dependency(%q<vcr>, [">= 0"])
|
data/test/test_batch.rb
CHANGED
@@ -5,6 +5,11 @@ class TestBatch < ParseTestCase
|
|
5
5
|
def test_initialize
|
6
6
|
batch = Parse::Batch.new
|
7
7
|
assert_equal batch.class, Parse::Batch
|
8
|
+
assert_equal Parse.client, batch.client
|
9
|
+
|
10
|
+
batch = Parse::Batch.new(Parse::Client.new)
|
11
|
+
assert_equal batch.class, Parse::Batch
|
12
|
+
assert_not_equal Parse.client, batch.client
|
8
13
|
end
|
9
14
|
|
10
15
|
def test_add_request
|
@@ -91,6 +96,21 @@ class TestBatch < ParseTestCase
|
|
91
96
|
end
|
92
97
|
end
|
93
98
|
|
99
|
+
def test_update_nils_delete_keys
|
100
|
+
VCR.use_cassette('test_batch_update_nils_delete_keys', :record => :new_episodes) do
|
101
|
+
post = Parse::Object.new("BatchTestObject")
|
102
|
+
post["foo"] = "1"
|
103
|
+
post.save
|
104
|
+
|
105
|
+
post["foo"] = nil
|
106
|
+
batch = Parse::Batch.new
|
107
|
+
batch.update_object(post)
|
108
|
+
batch.run!
|
109
|
+
|
110
|
+
assert_false post.refresh.keys.include?("foo")
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
94
114
|
def test_delete_object
|
95
115
|
VCR.use_cassette('test_batch_delete_object', :record => :new_episodes) do
|
96
116
|
objects = [1, 2, 3, 4, 5].map do |i|
|
data/test/test_datatypes.rb
CHANGED
@@ -18,14 +18,13 @@ class TestDatatypes < Test::Unit::TestCase
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def test_date
|
21
|
-
date_time =
|
22
|
-
|
23
|
-
parse_date = Parse::Date.new data
|
21
|
+
date_time = Time.at(0).to_datetime
|
22
|
+
parse_date = Parse::Date.new(date_time)
|
24
23
|
|
25
|
-
assert_equal parse_date.value
|
26
|
-
assert_equal JSON.parse(parse_date.to_json)["iso"]
|
24
|
+
assert_equal date_time, parse_date.value
|
25
|
+
assert_equal "1970-01-01T00:00:00.000Z", JSON.parse(parse_date.to_json)["iso"]
|
27
26
|
assert_equal 0, parse_date <=> parse_date
|
28
|
-
assert_equal 0, Parse::Date.new(
|
27
|
+
assert_equal 0, Parse::Date.new(date_time) <=> Parse::Date.new(date_time)
|
29
28
|
|
30
29
|
post = Parse::Object.new("Post")
|
31
30
|
post["time"] = parse_date
|
data/test/test_object.rb
CHANGED
@@ -143,11 +143,19 @@ class TestObject < ParseTestCase
|
|
143
143
|
end
|
144
144
|
end
|
145
145
|
|
146
|
+
def test_saving_nested_objects
|
147
|
+
VCR.use_cassette('test_saving_nested_objects', :record => :new_episodes) do
|
148
|
+
post = Parse::Object.new "Post"
|
149
|
+
post["comment"] = Parse::Object.new("Comment", "text" => "testing")
|
150
|
+
assert_raise{post.save}
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
146
154
|
def test_boolean_values_as_json
|
147
155
|
post = Parse::Object.new "Post"
|
148
156
|
post["read"] = false
|
149
157
|
post["published"] = true
|
150
|
-
safe_json_hash = JSON.parse post.
|
158
|
+
safe_json_hash = JSON.parse post.safe_hash.to_json
|
151
159
|
assert_equal false, safe_json_hash["read"]
|
152
160
|
assert_equal true, safe_json_hash["published"]
|
153
161
|
end
|
@@ -269,17 +277,4 @@ class TestObject < ParseTestCase
|
|
269
277
|
assert_equal 'baz', bar['baz']
|
270
278
|
end
|
271
279
|
end
|
272
|
-
|
273
|
-
def test_circular_save
|
274
|
-
VCR.use_cassette('test_circular_save', :record => :new_episodes) do
|
275
|
-
bar = Parse::Object.new("CircularBar", "text" => "bar")
|
276
|
-
bar_2 = Parse::Object.new("CircularBar", "bar" => bar, "text" => "bar_2")
|
277
|
-
bar_2.save
|
278
|
-
bar['bar'] = bar_2
|
279
|
-
assert bar.save
|
280
|
-
|
281
|
-
assert_equal "bar_2", bar["bar"]["text"]
|
282
|
-
assert_equal "bar", bar["bar"]["bar"]["text"]
|
283
|
-
end
|
284
|
-
end
|
285
280
|
end
|