parse-ruby-client 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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