appengine-apis 0.0.8 → 0.0.9

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,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