parse-ruby-client 0.1.15 → 0.2.0
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.
- 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
|