parse-ruby-client 0.2.0 → 0.3.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.
Files changed (49) hide show
  1. checksums.yaml +15 -0
  2. data/.travis.yml +2 -1
  3. data/Gemfile +4 -6
  4. data/Gemfile.lock +18 -18
  5. data/README.md +1 -1
  6. data/Rakefile +1 -1
  7. data/VERSION +1 -1
  8. data/example.rb +2 -1
  9. data/fixtures/vcr_cassettes/test_batch_update_nils_delete_keys.yml +57 -0
  10. data/fixtures/vcr_cassettes/test_empty_response.yml +72 -0
  11. data/fixtures/vcr_cassettes/test_get_missing.yml +95 -38
  12. data/fixtures/vcr_cassettes/test_image_file_associate_with_object.yml +2089 -0
  13. data/fixtures/vcr_cassettes/test_image_file_save.yml +1928 -0
  14. data/fixtures/vcr_cassettes/test_object_id.yml +56 -0
  15. data/fixtures/vcr_cassettes/test_reset_password.yml +109 -0
  16. data/fixtures/vcr_cassettes/test_retries.yml +357 -0
  17. data/fixtures/vcr_cassettes/test_retries_404.yml +72 -0
  18. data/fixtures/vcr_cassettes/test_retries_404_correct.yml +72 -0
  19. data/fixtures/vcr_cassettes/test_retries_json_error.yml +357 -0
  20. data/fixtures/vcr_cassettes/test_retries_server_error.yml +357 -0
  21. data/fixtures/vcr_cassettes/test_server_update.yml +248 -191
  22. data/fixtures/vcr_cassettes/test_user_login.yml +276 -0
  23. data/fixtures/vcr_cassettes/test_xget.yml +280 -0
  24. data/lib/faraday/better_retry.rb +94 -0
  25. data/lib/faraday/extended_parse_json.rb +39 -0
  26. data/lib/faraday/get_method_override.rb +32 -0
  27. data/lib/parse-ruby-client.rb +9 -9
  28. data/lib/parse/batch.rb +2 -3
  29. data/lib/parse/client.rb +72 -161
  30. data/lib/parse/cloud.rb +2 -1
  31. data/lib/parse/datatypes.rb +6 -1
  32. data/lib/parse/error.rb +7 -3
  33. data/lib/parse/installation.rb +18 -0
  34. data/lib/parse/model.rb +2 -1
  35. data/lib/parse/object.rb +7 -1
  36. data/lib/parse/protocol.rb +10 -0
  37. data/lib/parse/push.rb +3 -0
  38. data/lib/parse/query.rb +5 -2
  39. data/lib/parse/user.rb +1 -0
  40. data/lib/parse/util.rb +2 -0
  41. data/parse-ruby-client.gemspec +35 -15
  42. data/test/helper.rb +55 -1
  43. data/test/middleware/better_retry_test.rb +57 -0
  44. data/test/middleware/extend_parse_json_test.rb +55 -0
  45. data/test/test_client.rb +78 -28
  46. data/test/test_datatypes.rb +14 -0
  47. data/test/test_object.rb +16 -0
  48. metadata +39 -34
  49. data/lib/parse/http_client.rb +0 -84
@@ -0,0 +1,94 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'ostruct'
3
+ module Faraday
4
+ # Catches exceptions and retries each request a limited number of times.
5
+ #
6
+ # By default, it retries 2 times and handles only timeout exceptions. It can
7
+ # be configured with an arbitrary number of retries, a list of exceptions to
8
+ # handle an a retry interval.
9
+ #
10
+ # Examples
11
+ #
12
+ # Faraday.new do |conn|
13
+ # conn.request :retry, max: 2, interval: 0.05,
14
+ # exceptions: [CustomException, 'Timeout::Error']
15
+ # conn.adapter ...
16
+ # end
17
+ class BetterRetry < Faraday::Middleware
18
+ class Options < OpenStruct
19
+
20
+ def max
21
+ (self[:max] ||= 2).to_i
22
+ end
23
+
24
+ def interval
25
+ (self[:interval] ||= 0).to_f
26
+ end
27
+
28
+ def exceptions
29
+ Array(self[:exceptions] ||= [Errno::ETIMEDOUT, 'Timeout::Error', Error::TimeoutError])
30
+ end
31
+
32
+ # define for ruby less than 2.0
33
+ def [](name)
34
+ @table[name.to_sym]
35
+ end
36
+
37
+ def []=(name, value)
38
+ modifiable[new_ostruct_member(name)] = value
39
+ end
40
+
41
+ end
42
+
43
+ # Public: Initialize middleware
44
+ #
45
+ # Options:
46
+ # max - Maximum number of retries (default: 2).
47
+ # interval - Pause in seconds between retries (default: 0).
48
+ # exceptions - The list of exceptions to handle. Exceptions can be
49
+ # given as Class, Module, or String. (default:
50
+ # [Errno::ETIMEDOUT, Timeout::Error, Error::TimeoutError])
51
+ def initialize(app, options = {})
52
+ super(app)
53
+ @options = Options.new(options)
54
+ @errmatch = build_exception_matcher(@options.exceptions)
55
+ @logger = options[:logger]
56
+ end
57
+
58
+ def call(env)
59
+ env[:retries] = retries = @options.max
60
+ begin
61
+ @app.call(env)
62
+ rescue @errmatch => e
63
+ if retries > 0
64
+ if @logger
65
+ @logger.warn("Retrying Parse Error #{e.inspect} on request #{env[:url].to_s} #{env[:body].inspect} response #{env[:response].inspect}")
66
+ end
67
+ retries -= 1
68
+ env[:retries] = retries
69
+ sleep @options.interval if @options.interval > 0
70
+ retry
71
+ end
72
+ raise
73
+ end
74
+ end
75
+
76
+ # Private: construct an exception matcher object.
77
+ #
78
+ # An exception matcher for the rescue clause can usually be any object that
79
+ # responds to `===`, but for Ruby 1.8 it has to be a Class or Module.
80
+ def build_exception_matcher(exceptions)
81
+ matcher = Module.new
82
+ (class << matcher; self; end).class_eval do
83
+ define_method(:===) do |error|
84
+ exceptions.any? do |ex|
85
+ if ex.is_a? Module then error.is_a? ex
86
+ else error.class.to_s == ex.to_s
87
+ end
88
+ end
89
+ end
90
+ end
91
+ matcher
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,39 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Faraday
3
+
4
+ class ExtendedParseJson < FaradayMiddleware::ParseJson
5
+
6
+ def process_response(env)
7
+ env[:raw_body] = env[:body] if preserve_raw?(env)
8
+
9
+
10
+ if env[:status] >= 400
11
+ data = parse(env[:body]) || {} rescue {}
12
+
13
+ array_codes = [
14
+ Parse::Protocol::ERROR_INTERNAL,
15
+ Parse::Protocol::ERROR_TIMEOUT,
16
+ Parse::Protocol::ERROR_EXCEEDED_BURST_LIMIT
17
+ ]
18
+ error_hash = { "error" => "HTTP Status #{env[:status]} Body #{env[:body]}" }.merge(data)
19
+ if data['code'] && array_codes.include?(data['code'])
20
+ sleep 60 if data['code'] == Parse::Protocol::ERROR_EXCEEDED_BURST_LIMIT
21
+ raise exception(env).new(error_hash.merge(data))
22
+ elsif env[:status] >= 500
23
+ raise exception(env).new(error_hash.merge(data))
24
+ end
25
+ raise Parse::ParseProtocolError.new(error_hash)
26
+ else
27
+ data = parse(env[:body]) || {}
28
+
29
+ env[:body] = data
30
+ end
31
+ end
32
+
33
+ def exception env
34
+ # decide to retry or not
35
+ (env[:retries].to_i.zero? ? Parse::ParseProtocolError : Parse::ParseProtocolRetry)
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,32 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'faraday'
4
+
5
+ module Faraday
6
+ # Public: Writes the original HTTP method to "X-Http-Method-Override" header
7
+ # and sends the request as POST for GET requests that are too long.
8
+ class GetMethodOverride < Faraday::Middleware
9
+
10
+ HEADER = "X-Http-Method-Override".freeze
11
+
12
+ # Public: Initialize the middleware.
13
+ #
14
+ # app - the Faraday app to wrap
15
+ def initialize(app, options = nil)
16
+ super(app)
17
+ end
18
+
19
+ def call(env)
20
+ if env[:method] == :get && env[:url].to_s.size > 2000
21
+ env[:request_headers][HEADER] = 'GET'
22
+ env[:request_headers]['Content-Type'] =
23
+ 'application/x-www-form-urlencoded'
24
+ env[:body] = env[:url].query
25
+ env[:url].query = nil
26
+ env[:method] = :post
27
+ end
28
+
29
+ @app.call(env)
30
+ end
31
+ end
32
+ end
@@ -1,3 +1,4 @@
1
+ # -*- encoding : utf-8 -*-
1
2
  ## ----------------------------------------------------------------------
2
3
  ##
3
4
  ## Ruby client for parse.com
@@ -5,18 +6,16 @@
5
6
  ## See https://parse.com/docs/rest for full documentation on the API.
6
7
  ##
7
8
  ## ----------------------------------------------------------------------
8
- require "rubygems"
9
- require "bundler/setup"
9
+ require 'rubygems'
10
+ require 'bundler/setup'
10
11
 
11
- require 'json'
12
+ require 'faraday'
13
+ require 'faraday_middleware'
14
+ require 'faraday/better_retry'
15
+ require 'faraday/extended_parse_json'
16
+ require 'faraday/get_method_override'
12
17
  require 'date'
13
18
  require 'cgi'
14
- if defined?(JRUBY_VERSION)
15
- require 'net/http'
16
- require 'net/https'
17
- else
18
- require 'patron'
19
- end
20
19
 
21
20
  cwd = Pathname(__FILE__).dirname
22
21
  $:.unshift(cwd.to_s) unless $:.include?(cwd.to_s) || $:.include?(cwd.expand_path.to_s)
@@ -27,6 +26,7 @@ require 'parse/datatypes'
27
26
  require 'parse/util'
28
27
  require 'parse/protocol'
29
28
  require 'parse/user'
29
+ require "parse/installation"
30
30
  require 'parse/push'
31
31
  require 'parse/cloud'
32
32
  require 'parse/model'
@@ -1,5 +1,4 @@
1
- require 'json'
2
-
1
+ # -*- encoding : utf-8 -*-
3
2
  module Parse
4
3
  class Batch
5
4
  attr_reader :requests
@@ -51,4 +50,4 @@ module Parse
51
50
 
52
51
  end
53
52
 
54
- end
53
+ end
@@ -1,12 +1,9 @@
1
+ # -*- encoding : utf-8 -*-
1
2
  require 'parse/protocol'
2
3
  require 'parse/error'
3
4
  require 'parse/util'
4
- require 'parse/http_client'
5
5
 
6
6
  require 'logger'
7
-
8
- require 'iron_mq'
9
-
10
7
  module Parse
11
8
 
12
9
  # A class which encapsulates the HTTPS communication with the Parse
@@ -21,7 +18,7 @@ module Parse
21
18
  attr_accessor :max_retries
22
19
  attr_accessor :logger
23
20
 
24
- def initialize(data = {})
21
+ def initialize(data = {}, &blk)
25
22
  @host = data[:host] || Protocol::HOST
26
23
  @application_id = data[:application_id]
27
24
  @api_key = data[:api_key]
@@ -29,27 +26,26 @@ module Parse
29
26
  @session_token = data[:session_token]
30
27
  @max_retries = data[:max_retries] || 3
31
28
  @logger = data[:logger] || Logger.new(STDERR).tap{|l| l.level = Logger::INFO}
32
- @session = data[:http_client] || Parse::DEFAULT_HTTP_CLIENT.new
33
29
 
34
- if data[:ironio_project_id] && data[:ironio_token]
30
+ options = {:request => {:timeout => 30, :open_timeout => 30}}
35
31
 
36
- if data[:max_concurrent_requests]
37
- @max_concurrent_requests = data[:max_concurrent_requests]
38
- else
39
- @max_concurrent_requests = 50
40
- end
32
+ @session = Faraday.new("https://#{host}", options) do |c|
33
+ c.request :json
41
34
 
42
- @queue = IronMQ::Client.new({
43
- :project_id => data[:ironio_project_id],
44
- :token => data[:ironio_token]
45
- }).queue("concurrent_parse_requests")
35
+ c.use Faraday::GetMethodOverride
46
36
 
47
- end
37
+ c.use Faraday::BetterRetry,
38
+ max: @max_retries,
39
+ logger: @logger,
40
+ interval: 0.5,
41
+ exceptions: ['Faraday::Error::TimeoutError', 'Faraday::Error::ParsingError', 'Parse::ParseProtocolRetry']
42
+ c.use Faraday::ExtendedParseJson
43
+
44
+ c.response :logger, @logger
45
+ c.adapter Faraday.default_adapter
48
46
 
49
- @session.base_url = "https://#{host}"
50
- @session.headers["Content-Type"] = "application/json"
51
- @session.headers["Accept"] = "application/json"
52
- @session.headers["User-Agent"] = "Parse for Ruby, 0.0"
47
+ yield(c) if block_given?
48
+ end
53
49
  end
54
50
 
55
51
  # Perform an HTTP request for the given uri and method
@@ -57,95 +53,20 @@ module Parse
57
53
  # ParseProtocolError if the response has an error status code,
58
54
  # and will return the parsed JSON body on success, if there is one.
59
55
  def request(uri, method = :get, body = nil, query = nil, content_type = nil)
60
- options = {}
61
56
  headers = {}
62
57
 
63
- headers[Protocol::HEADER_MASTER_KEY] = @master_key if @master_key
64
- headers[Protocol::HEADER_API_KEY] = @api_key
65
- headers[Protocol::HEADER_APP_ID] = @application_id
66
- headers[Protocol::HEADER_SESSION_TOKEN] = @session_token if @session_token
67
-
68
- if body
69
- options[:data] = body
70
- end
71
- if query
72
- options[:query] = @session.build_query(query)
73
-
74
- # Avoid 502 or 414 when sending a large querystring. See https://parse.com/questions/502-error-when-query-with-huge-contains
75
- if options[:query].size > 2000 && method == :get && !body && !content_type
76
- options[:data] = options[:query]
77
- options[:query] = nil
78
- method = :post
79
- headers['X-HTTP-Method-Override'] = 'GET'
80
- content_type = 'application/x-www-form-urlencoded'
81
- end
58
+ {
59
+ "Content-Type" => content_type || 'application/json',
60
+ "User-Agent" => 'Parse for Ruby, 0.0',
61
+ Protocol::HEADER_MASTER_KEY => @master_key,
62
+ Protocol::HEADER_APP_ID => @application_id,
63
+ Protocol::HEADER_API_KEY => @api_key,
64
+ Protocol::HEADER_SESSION_TOKEN => @session_token,
65
+ }.each do |key, value|
66
+ headers[key] = value if value
82
67
  end
83
68
 
84
- if content_type
85
- headers["Content-Type"] = content_type
86
- end
87
-
88
- num_tries = 0
89
- begin
90
- num_tries += 1
91
-
92
- if @queue
93
-
94
- #while true
95
- # if @queue.reload.size >= @max_concurrent_requests
96
- # sleep 1
97
- # else
98
- # add to queue before request
99
- @queue.post("1")
100
- response = @session.request(method, uri, headers, options)
101
- # delete from queue after request
102
- msg = @queue.get()
103
- msg.delete
104
- # end
105
- #end
106
- else
107
- response = @session.request(method, uri, headers, options)
108
- end
109
-
110
- parsed = JSON.parse(response.body)
111
-
112
- if response.status >= 400
113
- parsed ||= {}
114
- raise ParseProtocolError.new({"error" => "HTTP Status #{response.status} Body #{response.body}"}.merge(parsed))
115
- end
116
-
117
- if content_type
118
- @session.headers["Content-Type"] = "application/json"
119
- end
120
-
121
- return parsed
122
- rescue JSON::ParserError => e
123
- if num_tries <= max_retries && response.status >= 500
124
- log_retry(e, uri, query, body, response)
125
- retry
126
- end
127
- raise
128
- rescue HttpClient::TimeoutError => e
129
- if num_tries <= max_retries
130
- log_retry(e, uri, query, body, response)
131
- retry
132
- end
133
- raise
134
- rescue ParseProtocolError => e
135
- if num_tries <= max_retries
136
- if e.code
137
- sleep 60 if e.code == Protocol::ERROR_EXCEEDED_BURST_LIMIT
138
- if [Protocol::ERROR_INTERNAL, Protocol::ERROR_TIMEOUT, Protocol::ERROR_EXCEEDED_BURST_LIMIT].include?(e.code)
139
- log_retry(e, uri, query, body, response)
140
- retry
141
- end
142
- elsif response.status >= 500
143
- log_retry(e, uri, query, body, response)
144
- retry
145
- end
146
- end
147
- raise
148
- end
69
+ @session.send(method, uri, query || body || {}, headers).body
149
70
  end
150
71
 
151
72
  def get(uri)
@@ -164,72 +85,62 @@ module Parse
164
85
  request(uri, :delete)
165
86
  end
166
87
 
167
- protected
168
-
169
- def log_retry(e, uri, query, body, response)
170
- logger.warn{"Retrying Parse Error #{e.inspect} on request #{uri} #{CGI.unescape(query.inspect)} #{body.inspect} response #{response.inspect}"}
171
- end
172
88
  end
173
89
 
174
90
 
175
91
  # Module methods
176
92
  # ------------------------------------------------------------
93
+ class << self
94
+ # A singleton client for use by methods in Object.
95
+ # Always use Parse.client to retrieve the client object.
96
+ @client = nil
97
+
98
+ # Initialize the singleton instance of Client which is used
99
+ # by all API methods. Parse.init must be called before saving
100
+ # or retrieving any objects.
101
+ def init(data = {}, &blk)
102
+ defaults = {:application_id => ENV["PARSE_APPLICATION_ID"], :api_key => ENV["PARSE_REST_API_KEY"]}
103
+ defaults.merge!(data)
104
+
105
+ # use less permissive key if both are specified
106
+ defaults[:master_key] = ENV["PARSE_MASTER_API_KEY"] unless data[:master_key] || defaults[:api_key]
107
+ @@client = Client.new(defaults, &blk)
108
+ end
177
109
 
178
- # A singleton client for use by methods in Object.
179
- # Always use Parse.client to retrieve the client object.
180
- @@client = nil
181
-
182
- # Initialize the singleton instance of Client which is used
183
- # by all API methods. Parse.init must be called before saving
184
- # or retrieving any objects.
185
- def Parse.init(data = {})
186
- defaulted = {:application_id => ENV["PARSE_APPLICATION_ID"],
187
- :api_key => ENV["PARSE_REST_API_KEY"]}
188
- defaulted.merge!(data)
189
-
190
- # use less permissive key if both are specified
191
- defaulted[:master_key] = ENV["PARSE_MASTER_API_KEY"] unless data[:master_key] || defaulted[:api_key]
192
-
193
-
194
- @@client = Client.new(defaulted)
195
- end
196
-
197
- # A convenience method for using global.json
198
- def Parse.init_from_cloud_code(path="../config/global.json")
199
- global = JSON.parse(Object::File.open(path).read) # warning: toplevel constant File referenced by Parse::Object::File
200
- application_name = global["applications"]["_default"]["link"]
201
- application_id = global["applications"][application_name]["applicationId"]
202
- master_key = global["applications"][application_name]["masterKey"]
203
- Parse.init :application_id => application_id,
204
- :master_key => master_key
205
- end
206
-
207
- # Used mostly for testing. Lets you delete the api key global vars.
208
- def Parse.destroy
209
- @@client = nil
210
- self
211
- end
110
+ # A convenience method for using global.json
111
+ def init_from_cloud_code(path = "../config/global.json")
112
+ # warning: toplevel constant File referenced by Parse::Object::File
113
+ global = JSON.parse(Object::File.open(path).read)
114
+ application_name = global["applications"]["_default"]["link"]
115
+ application_id = global["applications"][application_name]["applicationId"]
116
+ master_key = global["applications"][application_name]["masterKey"]
117
+ self.init(:application_id => application_id, :master_key => master_key)
118
+ end
212
119
 
213
- def Parse.client
214
- if !@@client
215
- raise ParseError, "API not initialized"
120
+ # Used mostly for testing. Lets you delete the api key global vars.
121
+ def destroy
122
+ @@client = nil
123
+ self
216
124
  end
217
- @@client
218
- end
219
125
 
220
- # Perform a simple retrieval of a simple object, or all objects of a
221
- # given class. If object_id is supplied, a single object will be
222
- # retrieved. If object_id is not supplied, then all objects of the
223
- # given class will be retrieved and returned in an Array.
224
- def Parse.get(class_name, object_id = nil)
225
- data = Parse.client.get( Protocol.class_uri(class_name, object_id) )
226
- Parse.parse_json class_name, data
227
- rescue ParseProtocolError => e
228
- if e.code == Protocol::ERROR_OBJECT_NOT_FOUND_FOR_GET
229
- e.message += ": #{class_name}:#{object_id}"
126
+ def client
127
+ raise ParseError, "API not initialized" if !@@client
128
+ @@client
230
129
  end
231
130
 
232
- raise
131
+ # Perform a simple retrieval of a simple object, or all objects of a
132
+ # given class. If object_id is supplied, a single object will be
133
+ # retrieved. If object_id is not supplied, then all objects of the
134
+ # given class will be retrieved and returned in an Array.
135
+ def get(class_name, object_id = nil)
136
+ data = self.client.get( Protocol.class_uri(class_name, object_id) )
137
+ self.parse_json class_name, data
138
+ rescue ParseProtocolError => e
139
+ if e.code == Protocol::ERROR_OBJECT_NOT_FOUND_FOR_GET
140
+ e.message += ": #{class_name}:#{object_id}"
141
+ end
142
+ raise
143
+ end
233
144
  end
234
145
 
235
146
  end