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.
@@ -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(self.merge(Parse::Protocol::KEY_CLASS_NAME => class_name)) unless 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 = safe_json
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
- def as_json(*a)
78
+ # representation of object to send on saves
79
+ def safe_hash
137
80
  Hash[self.map do |key, value|
138
- value = if !value.nil?
139
- value.respond_to?(:as_json) ? value.as_json : value
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
- Protocol::DELETE_OP
88
+ [key, Parse.pointerize_value(value)]
142
89
  end
90
+ end.compact]
91
+ end
143
92
 
144
- [key, value]
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
- as_json.to_json(*a)
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
- Parse.parse_json class_name, response
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 string or a Hash as parsed
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)
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "parse-ruby-client"
8
- s.version = "0.1.15"
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-05-22"
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>, [">= 0"])
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>, [">= 0"])
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>, [">= 0"])
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|
@@ -18,14 +18,13 @@ class TestDatatypes < Test::Unit::TestCase
18
18
  end
19
19
 
20
20
  def test_date
21
- date_time = DateTime.now
22
- data = date_time
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, date_time
26
- assert_equal JSON.parse(parse_date.to_json)["iso"], date_time.iso8601(3)
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(data) <=> Parse::Date.new(data)
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.safe_json
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