leancloud-ruby-client 0.1.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 (99) hide show
  1. checksums.yaml +7 -0
  2. data/.travis.yml +9 -0
  3. data/Gemfile +16 -0
  4. data/Gemfile.lock +86 -0
  5. data/LICENSE.txt +20 -0
  6. data/README.md +1177 -0
  7. data/Rakefile +45 -0
  8. data/VERSION +1 -0
  9. data/example.rb +35 -0
  10. data/features.md +1111 -0
  11. data/fixtures/vcr_cassettes/test_acls_arent_objects.yml +274 -0
  12. data/fixtures/vcr_cassettes/test_array_add.yml +213 -0
  13. data/fixtures/vcr_cassettes/test_array_add_pointerizing.yml +380 -0
  14. data/fixtures/vcr_cassettes/test_array_add_unique.yml +319 -0
  15. data/fixtures/vcr_cassettes/test_batch_create_object.yml +107 -0
  16. data/fixtures/vcr_cassettes/test_batch_delete_object.yml +637 -0
  17. data/fixtures/vcr_cassettes/test_batch_run.yml +109 -0
  18. data/fixtures/vcr_cassettes/test_batch_update_nils_delete_keys.yml +435 -0
  19. data/fixtures/vcr_cassettes/test_batch_update_object.yml +637 -0
  20. data/fixtures/vcr_cassettes/test_contains_all.yml +1143 -0
  21. data/fixtures/vcr_cassettes/test_cql.yml +99 -0
  22. data/fixtures/vcr_cassettes/test_created_at.yml +109 -0
  23. data/fixtures/vcr_cassettes/test_decrement.yml +213 -0
  24. data/fixtures/vcr_cassettes/test_deep_parse.yml +321 -0
  25. data/fixtures/vcr_cassettes/test_destroy.yml +213 -0
  26. data/fixtures/vcr_cassettes/test_empty_response.yml +1026 -0
  27. data/fixtures/vcr_cassettes/test_eq_pointerize.yml +427 -0
  28. data/fixtures/vcr_cassettes/test_equality.yml +321 -0
  29. data/fixtures/vcr_cassettes/test_get.yml +215 -0
  30. data/fixtures/vcr_cassettes/test_get_installation.yml +58 -0
  31. data/fixtures/vcr_cassettes/test_get_missing.yml +160 -0
  32. data/fixtures/vcr_cassettes/test_image_file_associate_with_object.yml +2089 -0
  33. data/fixtures/vcr_cassettes/test_image_file_save.yml +1928 -0
  34. data/fixtures/vcr_cassettes/test_include.yml +321 -0
  35. data/fixtures/vcr_cassettes/test_new_model.yml +109 -0
  36. data/fixtures/vcr_cassettes/test_new_object.yml +109 -0
  37. data/fixtures/vcr_cassettes/test_nils_delete_keys.yml +319 -0
  38. data/fixtures/vcr_cassettes/test_object_id.yml +56 -0
  39. data/fixtures/vcr_cassettes/test_parse_delete.yml +421 -0
  40. data/fixtures/vcr_cassettes/test_pointer.yml +109 -0
  41. data/fixtures/vcr_cassettes/test_request_sms.yml +48 -0
  42. data/fixtures/vcr_cassettes/test_reset_password.yml +109 -0
  43. data/fixtures/vcr_cassettes/test_retries.yml +4173 -0
  44. data/fixtures/vcr_cassettes/test_retries_404.yml +1026 -0
  45. data/fixtures/vcr_cassettes/test_retries_404_correct.yml +1026 -0
  46. data/fixtures/vcr_cassettes/test_retries_json_error.yml +2265 -0
  47. data/fixtures/vcr_cassettes/test_retries_server_error.yml +2265 -0
  48. data/fixtures/vcr_cassettes/test_save_installation.yml +58 -0
  49. data/fixtures/vcr_cassettes/test_save_with_sub_objects.yml +484 -0
  50. data/fixtures/vcr_cassettes/test_saving_boolean_values.yml +215 -0
  51. data/fixtures/vcr_cassettes/test_saving_nested_objects.yml +62 -0
  52. data/fixtures/vcr_cassettes/test_server_update.yml +586 -0
  53. data/fixtures/vcr_cassettes/test_simple_save.yml +109 -0
  54. data/fixtures/vcr_cassettes/test_text_file_save.yml +109 -0
  55. data/fixtures/vcr_cassettes/test_update.yml +213 -0
  56. data/fixtures/vcr_cassettes/test_updated_at.yml +213 -0
  57. data/fixtures/vcr_cassettes/test_user_login.yml +276 -0
  58. data/fixtures/vcr_cassettes/test_user_save.yml +109 -0
  59. data/fixtures/vcr_cassettes/test_xget.yml +603 -0
  60. data/leancloud-ruby-client.gemspec +166 -0
  61. data/lib/faraday/better_retry.rb +94 -0
  62. data/lib/faraday/extended_parse_json.rb +39 -0
  63. data/lib/faraday/get_method_override.rb +32 -0
  64. data/lib/leancloud-ruby-client.rb +34 -0
  65. data/lib/leancloud/application.rb +7 -0
  66. data/lib/leancloud/batch.rb +53 -0
  67. data/lib/leancloud/client.rb +149 -0
  68. data/lib/leancloud/cloud.rb +28 -0
  69. data/lib/leancloud/datatypes.rb +355 -0
  70. data/lib/leancloud/error.rb +42 -0
  71. data/lib/leancloud/installation.rb +57 -0
  72. data/lib/leancloud/model.rb +14 -0
  73. data/lib/leancloud/object.rb +252 -0
  74. data/lib/leancloud/protocol.rb +193 -0
  75. data/lib/leancloud/push.rb +48 -0
  76. data/lib/leancloud/query.rb +194 -0
  77. data/lib/leancloud/user.rb +38 -0
  78. data/lib/leancloud/util.rb +93 -0
  79. data/test/cloud_functions/MyCloudCode/cloud/main.js +4 -0
  80. data/test/config/global.json +14 -0
  81. data/test/helper.rb +108 -0
  82. data/test/middleware/better_retry_test.rb +57 -0
  83. data/test/middleware/extend_parse_json_test.rb +55 -0
  84. data/test/parsers.jpg +0 -0
  85. data/test/test_batch.rb +132 -0
  86. data/test/test_client.rb +183 -0
  87. data/test/test_cloud.rb +31 -0
  88. data/test/test_datatypes.rb +105 -0
  89. data/test/test_file.rb +67 -0
  90. data/test/test_init.rb +23 -0
  91. data/test/test_init_from_cloud_code.rb +8 -0
  92. data/test/test_installation.rb +49 -0
  93. data/test/test_model.rb +22 -0
  94. data/test/test_object.rb +295 -0
  95. data/test/test_push.rb +45 -0
  96. data/test/test_query.rb +198 -0
  97. data/test/test_throttle.rb +5 -0
  98. data/test/test_user.rb +60 -0
  99. metadata +298 -0
@@ -0,0 +1,48 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'cgi'
3
+ require 'leancloud/error'
4
+
5
+ module AV
6
+ class Push
7
+ attr_accessor :channels
8
+ attr_accessor :channel
9
+ attr_accessor :where
10
+ attr_accessor :type
11
+ attr_accessor :expiration_time_interval
12
+ attr_accessor :expiration_time
13
+ attr_accessor :push_time
14
+ attr_accessor :data
15
+ attr_accessor :production
16
+
17
+ def initialize(data, channel = "")
18
+ @data = data
19
+ @channel = channel
20
+ end
21
+
22
+ def save
23
+ uri = Protocol.push_uri
24
+
25
+ body = { :data => @data, :channel => @channel }
26
+
27
+ if @channels
28
+ body.merge!({ :channels => @channels })
29
+ body.delete :channel
30
+ end
31
+
32
+ if @where
33
+ body.merge!({ :where => @where })
34
+ body.delete :channel
35
+ end
36
+
37
+ body.merge!({ :expiration_interval => @expiration_time_interval }) if @expiration_time_interval
38
+ body.merge!({ :expiration_time => @expiration_time }) if @expiration_time
39
+ body.merge!({ :push_time => @push_time }) if @push_time
40
+ body.merge!({ :type => @type }) if @type
41
+ body.merge!({ :prod => 'dev' }) if not @production
42
+
43
+ response = AV.client.request uri, :post, body.to_json, nil
44
+ end
45
+
46
+ end
47
+
48
+ end
@@ -0,0 +1,194 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'cgi'
3
+
4
+ module AV
5
+
6
+ class Query
7
+ attr_accessor :where
8
+ attr_accessor :class_name
9
+ attr_accessor :order_by
10
+ attr_accessor :order
11
+ attr_accessor :limit
12
+ attr_accessor :skip
13
+ attr_accessor :count
14
+ attr_accessor :include
15
+
16
+ def self.do_cloud_query(cql, pvalues=[])
17
+ uri = Protocol.cql_uri
18
+ query = { "cql" => cql,"pvalues"=> pvalues.to_json }
19
+ AV.client.logger.info{"Leancloud query for #{uri} #{query.inspect}"} unless AV.client.quiet
20
+ response = AV.client.request uri, :get, nil, query
21
+
22
+ if response.is_a?(Hash) && response.has_key?(Protocol::KEY_RESULTS) && response[Protocol::KEY_RESULTS].is_a?(Array)
23
+ class_name = response[Protocol::KEY_CLASS_NAME]
24
+ parsed_results = response[Protocol::KEY_RESULTS].map{|o| AV.parse_json(class_name, o)}
25
+ return {
26
+ count: response['count'],
27
+ results: parsed_results
28
+ }
29
+ else
30
+ raise AVError.new("query response not a Hash with #{Protocol::KEY_RESULTS} key: #{response.class} #{response.inspect}")
31
+ end
32
+ end
33
+
34
+ def initialize(cls_name)
35
+ @class_name = cls_name
36
+ @where = {}
37
+ @order = :ascending
38
+ @ors = []
39
+ end
40
+
41
+ def add_constraint(field, constraint)
42
+ raise ArgumentError, "cannot add constraint to an $or query" if @ors.size > 0
43
+ current = where[field]
44
+ if current && current.is_a?(Hash) && constraint.is_a?(Hash)
45
+ current.merge! constraint
46
+ else
47
+ where[field] = constraint
48
+ end
49
+ end
50
+ #private :add_constraint
51
+
52
+ def includes(class_name)
53
+ @includes = class_name
54
+ self
55
+ end
56
+
57
+ def or(query)
58
+ raise ArgumentError, "you must pass an entire #{self.class} to \#or" unless query.is_a?(self.class)
59
+ @ors << query
60
+ self
61
+ end
62
+
63
+ def eq(hash_or_field,value=nil)
64
+ return eq_pair(hash_or_field,value) unless hash_or_field.is_a?(Hash)
65
+ hash_or_field.each_pair { |k,v| eq_pair k, v }
66
+ self
67
+ end
68
+
69
+ def eq_pair(field, value)
70
+ add_constraint field, AV.pointerize_value(value)
71
+ self
72
+ end
73
+
74
+ def not_eq(field, value)
75
+ add_constraint field, { "$ne" => AV.pointerize_value(value) }
76
+ self
77
+ end
78
+
79
+ def regex(field, expression)
80
+ add_constraint field, { "$regex" => expression }
81
+ self
82
+ end
83
+
84
+ def less_than(field, value)
85
+ add_constraint field, { "$lt" => AV.pointerize_value(value) }
86
+ self
87
+ end
88
+
89
+ def less_eq(field, value)
90
+ add_constraint field, { "$lte" => AV.pointerize_value(value) }
91
+ self
92
+ end
93
+
94
+ def greater_than(field, value)
95
+ add_constraint field, { "$gt" => AV.pointerize_value(value) }
96
+ self
97
+ end
98
+
99
+ def greater_eq(field, value)
100
+ add_constraint field, { "$gte" => AV.pointerize_value(value) }
101
+ self
102
+ end
103
+
104
+ def value_in(field, values)
105
+ add_constraint field, { "$in" => values.map { |v| AV.pointerize_value(v) } }
106
+ self
107
+ end
108
+
109
+ def value_not_in(field, values)
110
+ add_constraint field, { "$nin" => values.map { |v| AV.pointerize_value(v) } }
111
+ self
112
+ end
113
+
114
+ def contains_all(field, values)
115
+ add_constraint field, { "$all" => values.map { |v| AV.pointerize_value(v) } }
116
+ self
117
+ end
118
+
119
+ def related_to(field,value)
120
+ h = {"object" => AV.pointerize_value(value), "key" => field}
121
+ add_constraint("$relatedTo", h )
122
+ end
123
+
124
+ def exists(field, value = true)
125
+ add_constraint field, { "$exists" => value }
126
+ self
127
+ end
128
+
129
+ def in_query(field, query=nil)
130
+ query_hash = {AV::Protocol::KEY_CLASS_NAME => query.class_name, "where" => query.where}
131
+ add_constraint(field, "$inQuery" => query_hash)
132
+ self
133
+ end
134
+
135
+ def count
136
+ @count = true
137
+ self
138
+ end
139
+
140
+ def where_as_json
141
+ if @ors.size > 0
142
+ {"$or" => [self.where] + @ors.map{|query| query.where_as_json}}
143
+ else
144
+ @where
145
+ end
146
+ end
147
+
148
+ def first
149
+ self.limit = 1
150
+ get.first
151
+ end
152
+
153
+ def get
154
+ uri = Protocol.class_uri @class_name
155
+ if @class_name == AV::Protocol::CLASS_USER
156
+ uri = Protocol.user_uri
157
+ elsif @class_name == AV::Protocol::CLASS_INSTALLATION
158
+ uri = Protocol.installation_uri
159
+ end
160
+ query = { "where" => where_as_json.to_json }
161
+ set_order(query)
162
+ [:count, :limit, :skip, :include].each {|a| merge_attribute(a, query)}
163
+ AV.client.logger.info{"Parse query for #{uri} #{query.inspect}"} unless AV.client.quiet
164
+ response = AV.client.request uri, :get, nil, query
165
+
166
+ if response.is_a?(Hash) && response.has_key?(Protocol::KEY_RESULTS) && response[Protocol::KEY_RESULTS].is_a?(Array)
167
+ parsed_results = response[Protocol::KEY_RESULTS].map{|o| AV.parse_json(class_name, o)}
168
+ if response.keys.size == 1
169
+ parsed_results
170
+ else
171
+ response.dup.merge(Protocol::KEY_RESULTS => parsed_results)
172
+ end
173
+ else
174
+ raise AVError.new("query response not a Hash with #{Protocol::KEY_RESULTS} key: #{response.class} #{response.inspect}")
175
+ end
176
+ end
177
+
178
+ private
179
+
180
+ def set_order(query)
181
+ return unless @order_by
182
+ order_string = @order_by
183
+ order_string = "-#{order_string}" if @order == :descending
184
+ query.merge!(:order => order_string)
185
+ end
186
+
187
+ def merge_attribute(attribute, query, query_field = nil)
188
+ value = self.instance_variable_get("@#{attribute.to_s}")
189
+ return if value.nil?
190
+ query.merge!((query_field || attribute) => value)
191
+ end
192
+ end
193
+
194
+ end
@@ -0,0 +1,38 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'leancloud/protocol'
3
+ require 'leancloud/client'
4
+ require 'leancloud/error'
5
+ require 'leancloud/object'
6
+
7
+ module AV
8
+ class User < AV::Object
9
+
10
+ def self.authenticate(username, password)
11
+ body = {
12
+ "username" => username,
13
+ "password" => password
14
+ }
15
+
16
+ response = AV.client.request(AV::Protocol::USER_LOGIN_URI, :get, nil, body)
17
+ AV.client.session_token = response[AV::Protocol::KEY_USER_SESSION_TOKEN]
18
+
19
+ new(response)
20
+ end
21
+
22
+ def self.reset_password(email)
23
+ body = {"email" => email}
24
+ AV.client.post(AV::Protocol::PASSWORD_RESET_URI, body.to_json)
25
+ end
26
+
27
+ def initialize(data = nil)
28
+ data["username"] = data[:username] if data[:username]
29
+ data["password"] = data[:password] if data[:password]
30
+ super(AV::Protocol::CLASS_USER, data)
31
+ end
32
+
33
+ def uri
34
+ Protocol.user_uri @parse_object_id
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,93 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ module AV
4
+
5
+ # Parse a JSON representation into a fully instantiated
6
+ # class. obj can be either a primitive or a Hash of primitives as parsed
7
+ # by JSON.parse
8
+ # @param class_name [Object]
9
+ # @param obj [Object]
10
+ def AV.parse_json(class_name, obj)
11
+ if obj.nil?
12
+ nil
13
+
14
+ # Array
15
+ elsif obj.is_a? Array
16
+ obj.collect { |o| parse_json(class_name, o) }
17
+
18
+ # Hash
19
+ elsif obj.is_a? Hash
20
+
21
+ # If it's a datatype hash
22
+ if obj.has_key?(Protocol::KEY_TYPE)
23
+ parse_datatype obj
24
+ elsif class_name # otherwise it must be a regular object, so deep parse it avoiding re-JSON.parsing raw Strings
25
+ AV::Object.new class_name, Hash[obj.map{|k,v| [k, parse_json(nil, v)]}]
26
+ else # plain old hash
27
+ obj
28
+ end
29
+
30
+ # primitive
31
+ else
32
+ obj
33
+ end
34
+ end
35
+
36
+ def AV.parse_datatype(obj)
37
+ type = obj[Protocol::KEY_TYPE]
38
+
39
+ case type
40
+ when Protocol::TYPE_POINTER
41
+ if obj[Protocol::KEY_CREATED_AT]
42
+ AV::Object.new obj[Protocol::KEY_CLASS_NAME], Hash[obj.map{|k,v| [k, parse_json(nil, v)]}]
43
+ else
44
+ AV::Pointer.new obj
45
+ end
46
+ when Protocol::TYPE_BYTES
47
+ AV::Bytes.new obj
48
+ when Protocol::TYPE_DATE
49
+ AV::Date.new obj
50
+ when Protocol::TYPE_GEOPOINT
51
+ AV::GeoPoint.new obj
52
+ when Protocol::TYPE_FILE
53
+ AV::File.new obj
54
+ when Protocol::TYPE_OBJECT # used for relation queries, e.g. "?include=post"
55
+ AV::Object.new obj[Protocol::KEY_CLASS_NAME], Hash[obj.map{|k,v| [k, parse_json(nil, v)]}]
56
+ end
57
+ end
58
+
59
+ def AV.pointerize_value(obj)
60
+ if obj.kind_of?(AV::Object)
61
+ p = obj.pointer
62
+ raise ArgumentError.new("new object used in context requiring pointer #{obj}") unless p
63
+ p
64
+ elsif obj.is_a?(Array)
65
+ obj.map do |v|
66
+ AV.pointerize_value(v)
67
+ end
68
+ elsif obj.is_a?(Hash)
69
+ Hash[obj.map do |k, v|
70
+ [k, AV.pointerize_value(v)]
71
+ end]
72
+ else
73
+ obj
74
+ end
75
+ end
76
+
77
+ def AV.object_pointer_equality?(a, b)
78
+ classes = [AV::Object, AV::Pointer]
79
+ return false unless classes.any? { |c| a.kind_of?(c) } && classes.any? { |c| b.kind_of?(c) }
80
+ return true if a.equal?(b)
81
+ return false if a.new? || b.new?
82
+
83
+ a.class_name == b.class_name && a.id == b.id
84
+ end
85
+
86
+ def AV.object_pointer_hash(v)
87
+ if v.new?
88
+ v.object_id
89
+ else
90
+ v.class_name.hash ^ v.id.hash
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,4 @@
1
+ Parse.Cloud.define('trivial', function(request, response) {
2
+ console.log(request);
3
+ response.success(request.params);
4
+ });
@@ -0,0 +1,14 @@
1
+ {
2
+ "applications": {
3
+ "_default": {
4
+ "link": "FakeApp"
5
+ },
6
+ "FakeApp": {
7
+ "applicationId": "fakeApplicationId",
8
+ "masterKey": "fakeMasterKey"
9
+ }
10
+ },
11
+ "global": {
12
+ "parseVersion": "1.0.23"
13
+ }
14
+ }
data/test/helper.rb ADDED
@@ -0,0 +1,108 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'rubygems'
3
+ require 'bundler'
4
+ begin
5
+ Bundler.setup(:default, :development)
6
+ rescue Bundler::BundlerError => e
7
+ $stderr.puts e.message
8
+ $stderr.puts "Run `bundle install` to install missing gems"
9
+ exit e.status_code
10
+ end
11
+
12
+ require 'simplecov'
13
+ SimpleCov.start do
14
+ add_filter "/test/"
15
+ end if ENV["COVERAGE"]
16
+
17
+ require 'test/unit'
18
+ require 'shoulda'
19
+ require 'mocha'
20
+ require 'vcr'
21
+ require 'webmock/test_unit'
22
+
23
+ require 'simplecov'
24
+ SimpleCov.start if ENV['COVERAGE']
25
+
26
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
27
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
28
+ require 'leancloud-ruby-client'
29
+
30
+ YAML::ENGINE.yamler='syck' # get ascii strings as strings in fixtures
31
+
32
+ VCR.configure do |c|
33
+ c.cassette_library_dir = 'fixtures/vcr_cassettes'
34
+ c.hook_into :webmock # or :fakeweb
35
+ c.allow_http_connections_when_no_cassette = true
36
+ c.filter_sensitive_data("<COOKIE-KEY>") { |i| [i.response.headers['Set-Cookie']].flatten.compact.first }
37
+
38
+ def filter_sensitive_header(c, header)
39
+ c.filter_sensitive_data("<#{header}>") do |interaction|
40
+ if v = interaction.request.headers.detect{|k,_| k.casecmp(header) == 0}
41
+ v.last.first
42
+ end
43
+ end
44
+ end
45
+
46
+ filter_sensitive_header(c, AV::Protocol::HEADER_APP_ID)
47
+ filter_sensitive_header(c, AV::Protocol::HEADER_API_KEY)
48
+ filter_sensitive_header(c, AV::Protocol::HEADER_MASTER_KEY)
49
+ filter_sensitive_header(c, AV::Protocol::HEADER_SESSION_TOKEN)
50
+ end
51
+
52
+ class AVTestCase < Test::Unit::TestCase
53
+ def setup
54
+ @client = AV.init(:logger => Logger.new(STDERR).tap{|l| l.level = Logger::ERROR})
55
+ end
56
+ end
57
+
58
+ module Faraday
59
+ module LiveServerConfig
60
+ def live_server=(value)
61
+ @@live_server = case value
62
+ when /^http/
63
+ URI(value)
64
+ when /./
65
+ URI('http://127.0.0.1:4567')
66
+ end
67
+ end
68
+
69
+ def live_server?
70
+ defined? @@live_server
71
+ end
72
+
73
+ # Returns an object that responds to `host` and `port`.
74
+ def live_server
75
+ live_server? and @@live_server
76
+ end
77
+ end
78
+ class TestCase < Test::Unit::TestCase
79
+ extend LiveServerConfig
80
+ self.live_server = ENV['LIVE']
81
+
82
+ def test_default
83
+ assert true
84
+ end unless defined? ::MiniTest
85
+
86
+ def capture_warnings
87
+ old, $stderr = $stderr, StringIO.new
88
+ begin
89
+ yield
90
+ $stderr.string
91
+ ensure
92
+ $stderr = old
93
+ end
94
+ end
95
+
96
+ def self.jruby?
97
+ defined? RUBY_ENGINE and 'jruby' == RUBY_ENGINE
98
+ end
99
+
100
+ def self.rbx?
101
+ defined? RUBY_ENGINE and 'rbx' == RUBY_ENGINE
102
+ end
103
+
104
+ def self.ssl_mode?
105
+ ENV['SSL'] == 'yes'
106
+ end
107
+ end
108
+ end