appengine-apis 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,52 @@
1
+ #! /usr/bin/ruby
2
+ # Copyright:: Copyright 2009 Google Inc.
3
+ # Original Author:: Ryan Brown (mailto:ribrdb@google.com)
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+ # Replace TempFile with a StringIO.
18
+
19
+ $" << "tempfile.rb"
20
+
21
+ require 'stringio'
22
+
23
+ TempFile = Class.new(StringIO)
24
+
25
+ class Tempfile < StringIO
26
+ attr_reader :path
27
+
28
+ def initialize(basename, tmpdir=nil)
29
+ @path = basename
30
+ super()
31
+ end
32
+
33
+ def unlink; end
34
+
35
+ def close(*args)
36
+ super()
37
+ end
38
+
39
+ def open; end
40
+
41
+ alias close! close
42
+ alias delete unlink
43
+ alias length size
44
+
45
+ def self.open(*args)
46
+ if block_given?
47
+ yield new(*args)
48
+ else
49
+ new(*args)
50
+ end
51
+ end
52
+ end
@@ -69,6 +69,7 @@ module AppEngine
69
69
  # redirect chaininformation. If false, you see the HTTP response
70
70
  # yourself, including the 'Location' header, and redirects are not
71
71
  # followed.
72
+ # [:deadline] Deadline, in seconds, for the request.
72
73
  #
73
74
  # Returns a Net::HTTPResponse.
74
75
  #
@@ -103,6 +104,7 @@ module AppEngine
103
104
  headers = options.delete(:headers) || {}
104
105
  truncate = options.delete(:allow_truncated)
105
106
  follow_redirects = options.delete(:follow_redirects) || true
107
+ deadline = options.delete(:deadline)
106
108
 
107
109
  unless options.empty?
108
110
  raise ArgumentError, "Unsupported options #{options.inspect}."
@@ -125,6 +127,8 @@ module AppEngine
125
127
  options.do_not_follow_redirects
126
128
  end
127
129
 
130
+ options.set_deadline(deadline) if deadline
131
+
128
132
  url = java.net.URL.new(url) unless url.java_kind_of? java.net.URL
129
133
  request = HTTPRequest.new(url, method, options)
130
134
 
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/ruby1.8 -w
2
+ #
3
+ # Copyright:: Copyright 2009 Google Inc.
4
+ # Original Author:: Ryan Brown (mailto:ribrdb@google.com)
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ # XMPP API.
19
+
20
+ require 'appengine-apis/apiproxy'
21
+ require 'logger'
22
+
23
+ module AppEngine
24
+
25
+ # The XMPP api provides an interface for accessing XMPP status information,
26
+ # sending XMPP messages, and parsing XMPP responses.
27
+ module XMPP
28
+ module Proto
29
+ %w(PresenceRequest PresenceResponse
30
+ XmppInviteRequest XmppInviteResponse
31
+ XmppMessageRequest XmppMessageResponse
32
+ XmppServiceError).each do |name|
33
+ const_set(name, JavaUtilities.get_proxy_or_package_under_package(
34
+ com.google.appengine.api.xmpp, "XMPPServicePb$#{name}"
35
+ ))
36
+ end
37
+ ErrorCode = XmppServiceError::ErrorCode
38
+ end
39
+
40
+ class XMPPError < StandardError; end
41
+
42
+ module Status
43
+ NO_ERROR = Proto::XmppMessageResponse::XmppMessageStatus::NO_ERROR.value
44
+ INVALID_JID =
45
+ Proto::XmppMessageResponse::XmppMessageStatus::INVALID_JID.value
46
+ OTHER_ERROR =
47
+ Proto::XmppMessageResponse::XmppMessageStatus::OTHER_ERROR.value
48
+ end
49
+
50
+ # Represents presence information returned by the server.
51
+ class Presence
52
+ def initialize(available)
53
+ @available = available
54
+ end
55
+
56
+ def available?
57
+ @available
58
+ end
59
+ end
60
+
61
+ # Represents an incoming or outgoing XMPP Message.
62
+ # Also includes support for parsing chat commands. Commands are of the form
63
+ # /{command} {arg}?
64
+ # A backslash is also recognized as the first character to support chat
65
+ # client which internally handle / commands.
66
+ class Message
67
+ ARG_INDEX = {:to => 0, :body => 1, :from => 2, :type => 3, :xml => 4}
68
+ COMMAND_REGEX = /^[\\\/](\S+)(\s+(.+))?/
69
+
70
+ attr_reader :type, :sender, :recipients, :body
71
+
72
+ # call-seq:
73
+ # Message.new(to, body, from=nil, type=:chat, xml=false)
74
+ # or
75
+ # Message.new(options)
76
+ #
77
+ # Constructor for sending an outgoing XMPP message or parsing
78
+ # an incoming XMPP message.
79
+ #
80
+ # Args / Options:
81
+ # [:to] Destination JID or array of JIDs for the message.
82
+ # [:body] Body of the message.
83
+ # [:from]
84
+ # Optional custom sender JID. The default is <appid>@appspot.com.
85
+ # Custom JIDs can be of the form <anything>@<appid>.appspotchat.com.
86
+ # [:type]
87
+ # Optional type. Valid types are :chat, :error, :groupchat,
88
+ # :headline, and :normal. See RFC 3921, section 2.1.1. The default
89
+ # is :chat.
90
+ # [:xml]
91
+ # If true specifies that the body should be interpreted as XML.
92
+ # If false, the contents of the body will be escaped and placed
93
+ # inside of a body element inside of the message. If true, the
94
+ # contents will be made children of the message.
95
+ def initialize(*args)
96
+ if args.size == 1
97
+ options = args[0]
98
+ elsif args[-1].kind_of? Hash
99
+ options = args.pop
100
+ else
101
+ options = {}
102
+ end
103
+ @recipients = fetch_arg(:to, options, args)
104
+ @body = fetch_arg(:body, options, args)
105
+ unless @recipients && @body
106
+ raise ArgumentError, "Recipient and body are required."
107
+ end
108
+ @recipients = [@recipients] unless @recipients.kind_of? Array
109
+
110
+ @sender = fetch_arg(:from, options, args)
111
+ @type = fetch_arg(:type, options, args) || :chat
112
+ @xml = !!fetch_arg(:xml, options, args)
113
+ end
114
+
115
+ def xml?
116
+ @xml
117
+ end
118
+
119
+ # Returns the command if this message contains a chat command.
120
+ def command
121
+ parse_command
122
+ @command
123
+ end
124
+
125
+ # If this message contains a chat command, returns the command argument.
126
+ # Otherwise, returns the message body.
127
+ def arg
128
+ parse_command
129
+ @arg
130
+ end
131
+
132
+ # Convenience method to reply to a message.
133
+ def reply(body, type=:chat, xml=false)
134
+ message = Message.new([sender], body, recipients[0], type, xml)
135
+ XMPP.send_message(message)
136
+ end
137
+
138
+ private
139
+ def parse_command
140
+ return if @arg
141
+ if body =~ COMMAND_REGEX
142
+ @command = $1
143
+ @arg = $3 || ''
144
+ else
145
+ @arg = body
146
+ end
147
+ end
148
+
149
+ def to_proto
150
+ proto = Proto::XmppMessageRequest.new
151
+ recipients.each do |jid|
152
+ proto.add_jid(jid)
153
+ end
154
+ proto.set_body(body)
155
+ proto.set_raw_xml(xml?)
156
+ proto.set_from_jid(sender) if sender
157
+ proto.set_type(type.to_s)
158
+ proto
159
+ end
160
+
161
+ def fetch_arg(name, options, args)
162
+ arg = options[name] || args[ARG_INDEX[name]]
163
+ unless arg.kind_of? String
164
+ if arg.respond_to? :read
165
+ arg = arg.read
166
+ elsif arg.kind_of? Hash
167
+ arg.each do |key, value|
168
+ if value.respond_to? :read
169
+ arg = value.read
170
+ break
171
+ end
172
+ end
173
+ end
174
+ end
175
+ arg
176
+ end
177
+ end
178
+
179
+ class << self
180
+
181
+ # Get the presence for a JID.
182
+ #
183
+ # Args:
184
+ # - jid: The JID of the contact whose presence is requested.
185
+ # - from_jid: Optional custom sender JID.
186
+ # The default is <appid>@appspot.com. Custom JIDs can be of the form
187
+ # <anything>@<appid>.appspotchat.com.
188
+ #
189
+ # Returns:
190
+ # - A Presence object.
191
+ def get_presence(jid, from_jid=nil)
192
+ raise ArgumentError, 'Jabber ID cannot be nil' if jid.nil?
193
+ request = Proto::PresenceRequest.new
194
+ request.set_jid(jid)
195
+ request.set_from_jid(from_jid) if from_jid
196
+
197
+ response = make_sync_call('GetPresence', request,
198
+ Proto::PresenceResponse)
199
+ Presence.new(response.isIsAvailable)
200
+ rescue ApiProxy::ApplicationException => ex
201
+ case Proto::ErrorCode.value_of(ex.application_error)
202
+ when Proto::ErrorCode::INVALID_JID
203
+ raise ArgumentError, "Invalid jabber ID: #{jid}"
204
+ else
205
+ raise XMPPError, 'Unknown error retrieving presence for jabber ID: ' +
206
+ jid
207
+ end
208
+ end
209
+
210
+ # Send a chat invitaion.
211
+ #
212
+ # Args:
213
+ # - jid: JID of the contact to invite.
214
+ # - from_jid: Optional custom sender JID.
215
+ # The default is <appid>@appspot.com. Custom JIDs can be of the form
216
+ # <anything>@<appid>.appspotchat.com.
217
+ def send_invitation(jid, from_jid=nil)
218
+ raise ArgumentError, 'Jabber ID cannot be nil' if jid.nil?
219
+ request = Proto::XmppInviteRequest.new
220
+ request.set_jid(jid)
221
+ request.set_from_jid(from_jid) if from_jid
222
+
223
+ make_sync_call('SendInvite', request, Proto::XmppInviteResponse)
224
+ nil
225
+ rescue ApiProxy::ApplicationException => ex
226
+ case Proto::ErrorCode.value_of(ex.application_error)
227
+ when Proto::ErrorCode::INVALID_JID
228
+ raise ArgumentError, "Invalid jabber ID: #{jid}"
229
+ else
230
+ raise XMPPError, 'Unknown error sending invitation to jabber ID: ' +
231
+ jid
232
+ end
233
+ end
234
+
235
+
236
+ # call-seq:
237
+ # XMPP.send_message(message)
238
+ # or
239
+ # XMPP.send_message(*message_args)
240
+ #
241
+ # Send a chat message.
242
+ #
243
+ # Args:
244
+ # - message: A Message object to send.
245
+ # - message_args: Used to create a new Message. See #Message.new
246
+ #
247
+ # Returns an Array Statuses, one for each JID, corresponding to the
248
+ # result of sending the message to that JID.
249
+ def send_message(*args)
250
+ if args[0].kind_of? Message
251
+ message = args[0]
252
+ else
253
+ message = Message.new(*args)
254
+ end
255
+ request = message.send :to_proto
256
+ response = make_sync_call('SendMessage', request,
257
+ Proto::XmppMessageResponse)
258
+ response.status_iterator.to_a
259
+ rescue ApiProxy::ApplicationException => ex
260
+ case Proto::ErrorCode.value_of(ex.application_error)
261
+ when Proto::ErrorCode::INVALID_JID
262
+ raise ArgumentError, "Invalid jabber ID"
263
+ when Proto::ErrorCode::NO_BODY
264
+ raise ArgumentError, "Missing message body"
265
+ when Proto::ErrorCode::INVALID_XML
266
+ raise ArgumentError, "Invalid XML body"
267
+ when Proto::ErrorCode::INVALID_TYPE
268
+ raise ArgumentError, "Invalid type #{message.type.inspect}"
269
+ else
270
+ raise XMPPError, 'Unknown error sending message'
271
+ end
272
+ end
273
+
274
+ private
275
+ def make_sync_call(call, request, response_class)
276
+ bytes = ApiProxy.make_sync_call('xmpp', call, request.to_byte_array)
277
+ response = response_class.new
278
+ response.merge_from(bytes)
279
+ return response
280
+ end
281
+ end
282
+ end
283
+ end
@@ -35,6 +35,14 @@ describe AppEngine::Datastore do
35
35
  stored[:b].should == "b"
36
36
  end
37
37
 
38
+ it 'should raise EntityNotFound' do
39
+ p = lambda do
40
+ key = Datastore::Key.from_path("Does", "not exist")
41
+ e = Datastore.get(key)
42
+ end
43
+ p.should raise_error Datastore::EntityNotFound
44
+ end
45
+
38
46
  it "should support Text" do
39
47
  entity = Datastore::Entity.new("Test")
40
48
  entity[:a] = Datastore::Text.new("a")
@@ -73,6 +81,25 @@ describe AppEngine::Datastore do
73
81
  p.should raise_error Datastore::TransactionFailed
74
82
  end
75
83
 
84
+ it "should support query transactions" do
85
+ a = Datastore::Entity.new("A")
86
+ Datastore.put(a)
87
+ b = Datastore::Entity.new("B", a.key)
88
+ b[:a] = 0
89
+ Datastore.put(b)
90
+ p = lambda do
91
+ Datastore.transaction do
92
+ b2 = Datastore::Query.new("B", a.key).entity
93
+ b[:a] += 1
94
+ Datastore.put(nil, b)
95
+ Datastore.put(b2)
96
+ end
97
+ end
98
+ pending("Local ancestory only queries") do
99
+ p.should raise_error Datastore::TransactionFailed
100
+ end
101
+ end
102
+
76
103
  it "should retry transactions" do
77
104
  a = Datastore::Entity.new("A")
78
105
  a[:a] = 0
@@ -92,6 +119,29 @@ describe AppEngine::Datastore do
92
119
  lambda {Datastore.transaction{ raise "Foo"}}.should raise_error "Foo"
93
120
  Datastore.active_transactions.to_a.should == []
94
121
  end
122
+
123
+ describe "extract_tx" do
124
+ it "should support a single key" do
125
+ key = Datastore::Key.from_path("A", 1)
126
+ Datastore.extract_tx([key]).should == [key]
127
+ end
128
+ end
129
+
130
+ it "should support allocate_ids" do
131
+ ids = Datastore.allocate_ids('Foo', 1)
132
+ ids.size.should == 1
133
+ ids.map{|x| x}.should == [ids.start]
134
+ ids.start.should == ids.end
135
+ end
136
+
137
+ it "should support allocate_ids with parent" do
138
+ parent = Datastore::Key.from_path("A", 1)
139
+ ids = Datastore.allocate_ids(parent, 'Foo', 2)
140
+ ids.size.should == 2
141
+ ids.start.id.should == ids.end.id - 1
142
+ ids.map {|x| x}.size.should == 2
143
+ ids.start.parent.should == parent
144
+ end
95
145
  end
96
146
 
97
147
  describe AppEngine::Datastore::Query do
@@ -159,5 +209,9 @@ describe AppEngine::Datastore::Query do
159
209
  q.sort('name', Query::DESCENDING)
160
210
  q.fetch.to_a.should == [@aa, @a]
161
211
  end
162
-
212
+
213
+ it "should support count" do
214
+ q = Query.new("A")
215
+ q.count.should == 2
216
+ end
163
217
  end
@@ -160,6 +160,57 @@ describe AppEngine::Datastore::Entity do
160
160
  @entity['time'].class.should == Time
161
161
  end
162
162
 
163
+ it "should support Email" do
164
+ email = "ribrdb@example.com"
165
+ @entity['email'] = AppEngine::Datastore::Email.new(email)
166
+ @entity['email'].should == email
167
+ @entity['email'].class.should == AppEngine::Datastore::Email
168
+ end
169
+
170
+ it "should support Category" do
171
+ category = "food"
172
+ @entity['cat'] = AppEngine::Datastore::Category.new(category)
173
+ @entity['cat'].should == category
174
+ @entity['cat'].class.should == AppEngine::Datastore::Category
175
+ end
176
+
177
+ it "should support PhoneNumbers" do
178
+ number = '555-1212'
179
+ @entity['phone'] = AppEngine::Datastore::PhoneNumber.new(number)
180
+ @entity['phone'].should == number
181
+ @entity['phone'].class.should == AppEngine::Datastore::PhoneNumber
182
+ end
183
+
184
+ it "should support PostalAddress" do
185
+ address = '345 Spear St'
186
+ @entity['address'] = AppEngine::Datastore::PostalAddress.new(address)
187
+ @entity['address'].should == address
188
+ @entity['address'].class.should == AppEngine::Datastore::PostalAddress
189
+ end
190
+
191
+ it "should support Rating" do
192
+ rating = 34
193
+ @entity['rating'] = AppEngine::Datastore::Rating.new(rating)
194
+ @entity['rating'].rating.should == rating
195
+ @entity['rating'].class.should == AppEngine::Datastore::Rating
196
+ end
197
+
198
+ it "should support IMHandle" do
199
+ im = AppEngine::Datastore::IMHandle.new(:xmpp, 'batman@google.com')
200
+ @entity['im'] = im
201
+ @entity['im'].should == im
202
+ @entity['im'].class.should == AppEngine::Datastore::IMHandle
203
+ end
204
+
205
+ it "should support GeoPt" do
206
+ latitude = 32.4
207
+ longitude = 72.2
208
+ @entity['address'] = AppEngine::Datastore::GeoPt.new(latitude, longitude)
209
+ @entity['address'].latitude.should be_close latitude, 0.001
210
+ @entity['address'].longitude.should be_close longitude, 0.001
211
+ @entity['address'].class.should == AppEngine::Datastore::GeoPt
212
+ end
213
+
163
214
  it "should support multiple values" do
164
215
  list = [1, 2, 3]
165
216
  @entity['list'] = list
@@ -211,3 +262,77 @@ describe AppEngine::Datastore::Text do
211
262
  end
212
263
  end
213
264
 
265
+ describe AppEngine::Datastore::Rating do
266
+ it 'should support ==' do
267
+ a = AppEngine::Datastore::Rating.new(27)
268
+ b = AppEngine::Datastore::Rating.new(27)
269
+ a.should == b
270
+ end
271
+
272
+ it 'should support <=>' do
273
+ a = AppEngine::Datastore::Rating.new(3)
274
+ b = AppEngine::Datastore::Rating.new(4)
275
+ a.should be < b
276
+ b.should be > a
277
+ end
278
+
279
+ it 'should check MIN_VALUE' do
280
+ l = lambda {AppEngine::Datastore::Rating.new -1}
281
+ l.should raise_error ArgumentError
282
+ end
283
+
284
+ it 'should check MAX_VALUE' do
285
+ l = lambda {AppEngine::Datastore::Rating.new 101}
286
+ l.should raise_error ArgumentError
287
+ end
288
+ end
289
+
290
+ describe AppEngine::Datastore::GeoPt do
291
+ it 'should support ==' do
292
+ a = AppEngine::Datastore::GeoPt.new(35, 62)
293
+ b = AppEngine::Datastore::GeoPt.new(a.latitude, a.longitude)
294
+ a.should == b
295
+ end
296
+
297
+ it 'should support <=>' do
298
+ a = AppEngine::Datastore::GeoPt.new(35, 62)
299
+ b = AppEngine::Datastore::GeoPt.new(36, 62)
300
+ a.should be < b
301
+ b.should be > a
302
+ end
303
+
304
+ it 'should convert exceptions' do
305
+ l = lambda {AppEngine::Datastore::GeoPt.new(700, 999)}
306
+ l.should raise_error ArgumentError
307
+ end
308
+ end
309
+
310
+ describe AppEngine::Datastore::IMHandle do
311
+ it 'should support ==' do
312
+ a = AppEngine::Datastore::IMHandle.new(:unknown, 'foobar')
313
+ b = AppEngine::Datastore::IMHandle.new(:unknown, 'foobar')
314
+ a.should == b
315
+ end
316
+
317
+ it 'should support symbols' do
318
+ p = Proc.new do
319
+ AppEngine::Datastore::IMHandle.new(:sip, "sip_address")
320
+ AppEngine::Datastore::IMHandle.new(:xmpp, "xmpp_address")
321
+ AppEngine::Datastore::IMHandle.new(:unknown, "unknown_address")
322
+ end
323
+ p.should_not raise_error ArgumentError
324
+ end
325
+
326
+ it 'should support urls' do
327
+ protocol = 'http://aim.com/'
328
+ address = 'foobar'
329
+ im = AppEngine::Datastore::IMHandle.new(protocol, address)
330
+ im.protocol.should == protocol
331
+ im.address.should == address
332
+ end
333
+
334
+ it 'should convert errors' do
335
+ l = lambda {AppEngine::Datastore::IMHandle.new 'aim', 'foobar'}
336
+ l.should raise_error ArgumentError
337
+ end
338
+ end