Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

first commit

  • Loading branch information...
commit 9c01fa6dc6ecff8b9ea461fd2a520c689780cff1 0 parents
@rubys authored
201 LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction,
+and distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by
+the copyright owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all
+other entities that control, are controlled by, or are under common
+control with that entity. For the purposes of this definition,
+"control" means (i) the power, direct or indirect, to cause the
+direction or management of such entity, whether by contract or
+otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity
+exercising permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications,
+including but not limited to software source code, documentation
+source, and configuration files.
+
+"Object" form shall mean any form resulting from mechanical
+transformation or translation of a Source form, including but
+not limited to compiled object code, generated documentation,
+and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or
+Object form, made available under the License, as indicated by a
+copyright notice that is included in or attached to the work
+(an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object
+form, that is based on (or derived from) the Work and for which the
+editorial revisions, annotations, elaborations, or other modifications
+represent, as a whole, an original work of authorship. For the purposes
+of this License, Derivative Works shall not include works that remain
+separable from, or merely link (or bind by name) to the interfaces of,
+the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including
+the original version of the Work and any modifications or additions
+to that Work or Derivative Works thereof, that is intentionally
+submitted to Licensor for inclusion in the Work by the copyright owner
+or by an individual or Legal Entity authorized to submit on behalf of
+the copyright owner. For the purposes of this definition, "submitted"
+means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems,
+and issue tracking systems that are managed by, or on behalf of, the
+Licensor for the purpose of discussing and improving the Work, but
+excluding communication that is conspicuously marked or otherwise
+designated in writing by the copyright owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity
+on behalf of whom a Contribution has been received by Licensor and
+subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+this License, each Contributor hereby grants to You a perpetual,
+worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the
+Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+this License, each Contributor hereby grants to You a perpetual,
+worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+(except as stated in this section) patent license to make, have made,
+use, offer to sell, sell, import, and otherwise transfer the Work,
+where such license applies only to those patent claims licensable
+by such Contributor that are necessarily infringed by their
+Contribution(s) alone or by combination of their Contribution(s)
+with the Work to which such Contribution(s) was submitted. If You
+institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work
+or a Contribution incorporated within the Work constitutes direct
+or contributory patent infringement, then any patent licenses
+granted to You under this License for that Work shall terminate
+as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+Work or Derivative Works thereof in any medium, with or without
+modifications, and in Source or Object form, provided that You
+meet the following conditions:
+
+(a) You must give any other recipients of the Work or
+Derivative Works a copy of this License; and
+
+(b) You must cause any modified files to carry prominent notices
+stating that You changed the files; and
+
+(c) You must retain, in the Source form of any Derivative Works
+that You distribute, all copyright, patent, trademark, and
+attribution notices from the Source form of the Work,
+excluding those notices that do not pertain to any part of
+the Derivative Works; and
+
+(d) If the Work includes a "NOTICE" text file as part of its
+distribution, then any Derivative Works that You distribute must
+include a readable copy of the attribution notices contained
+within such NOTICE file, excluding those notices that do not
+pertain to any part of the Derivative Works, in at least one
+of the following places: within a NOTICE text file distributed
+as part of the Derivative Works; within the Source form or
+documentation, if provided along with the Derivative Works; or,
+within a display generated by the Derivative Works, if and
+wherever such third-party notices normally appear. The contents
+of the NOTICE file are for informational purposes only and
+do not modify the License. You may add Your own attribution
+notices within Derivative Works that You distribute, alongside
+or as an addendum to the NOTICE text from the Work, provided
+that such additional attribution notices cannot be construed
+as modifying the License.
+
+You may add Your own copyright statement to Your modifications and
+may provide additional or different license terms and conditions
+for use, reproduction, or distribution of Your modifications, or
+for any such Derivative Works as a whole, provided Your use,
+reproduction, and distribution of the Work otherwise complies with
+the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+any Contribution intentionally submitted for inclusion in the Work
+by You to the Licensor shall be under the terms and conditions of
+this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify
+the terms of any separate license agreement you may have executed
+with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+names, trademarks, service marks, or product names of the Licensor,
+except as required for reasonable and customary use in describing the
+origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+agreed to in writing, Licensor provides the Work (and each
+Contributor provides its Contributions) on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied, including, without limitation, any warranties or conditions
+of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+PARTICULAR PURPOSE. You are solely responsible for determining the
+appropriateness of using or redistributing the Work and assume any
+risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+whether in tort (including negligence), contract, or otherwise,
+unless required by applicable law (such as deliberate and grossly
+negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special,
+incidental, or consequential damages of any character arising as a
+result of this License or out of the use or inability to use the
+Work (including but not limited to damages for loss of goodwill,
+work stoppage, computer failure or malfunction, or any and all
+other commercial damages or losses), even if such Contributor
+has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+the Work or Derivative Works thereof, You may choose to offer,
+and charge a fee for, acceptance of support, warranty, indemnity,
+or other liability obligations and/or rights consistent with this
+License. However, in accepting such obligations, You may act only
+on Your own behalf and on Your sole responsibility, not on behalf
+of any other Contributor, and only if You agree to indemnify,
+defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason
+of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+To apply the Apache License to your work, attach the following
+boilerplate notice, with the fields enclosed by brackets "[]"
+replaced with your own identifying information. (Don't include
+the brackets!) The text should be enclosed in the appropriate
+comment syntax for the file format. We also recommend that a
+file or class name and description of purpose be included on the
+same "printed page" as the copyright notice for easier
+identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
2  NOTICE
@@ -0,0 +1,2 @@
+This library is based on the wave-robot-python-client library
+http://code.google.com/p/wave-robot-python-client/source/checkout
15 README
@@ -0,0 +1,15 @@
+Port of Wave Robot Python Client to Ruby.
+<http://code.google.com/p/wave-robot-python-client/>
+
+Status:
+
+* Not tested with any server.
+
+* Unit tests pass.
+
+* samples, robot.rb and __init__.rb not yet ported from Python to Ruby.
+
+* APIs need a complete and thorough scrubbing. In particular, they look like
+ they were designed for Java, hastily ported to Python, and then even more
+ hastily ported to Ruby; the last step done by someone who was clueless
+ as to what was intended to be accomplished.
9 samples/dummy/app.yaml
@@ -0,0 +1,9 @@
+application: dummy-bot
+version: 1
+runtime: python
+api_version: 1
+
+handlers:
+- url: /_wave/.*
+ script: dummy.py
+
31 samples/dummy/dummy.rb
@@ -0,0 +1,31 @@
+"""Dummy robot only."""
+
+__author__ = 'davidbyttow@google.com (David Byttow)'
+
+from waveapi import events
+from waveapi import model
+from waveapi import robot
+
+
+def OnParticipantsChanged(properties, context):
+ """Invoked when any participants have been added/removed."""
+ added = properties['participantsAdded']
+ for p in added:
+ if p == 'dummy-tutorial@appspot.com':
+ Setup(context)
+ break
+
+
+def Setup(context):
+ """Called when this robot is first added to the wave."""
+ root_wavelet = context.GetRootWavelet()
+ root_wavelet.CreateBlip().GetDocument().SetText("I'm alive!")
+
+
+if __name__ == '__main__':
+ dummy = robot.Robot('Dummy',
+ image_url='http://dummy.appspot.com/icon.png',
+ profile_url='http://dummy.appspot.com/')
+ dummy.RegisterHandler(events.WAVELET_PARTICIPANTS_CHANGED,
+ OnParticipantsChanged)
+ dummy.Run()
7 src/waveapi/__init__.rb
@@ -0,0 +1,7 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2009 Google Inc. All Rights Reserved.
+
+"""Declares the api package."""
+
+__author__ = 'davidbyttow@google.com (David Byttow)'
148 src/waveapi/document.rb
@@ -0,0 +1,148 @@
+#!/usr/bin/ruby
+#
+# Copyright 2009 Google Inc. All Rights Reserved.
+
+"""Defines document-based classes.
+
+This module defines classes that are used to modify and describe documents and
+their operations.
+"""
+
+__author__ = 'davidbyttow@google.com (David Byttow)'
+
+require 'util'
+
+
+# class Range(object)
+# JAVA_CLASS = 'com.google.wave.api.Range'
+
+
+class Annotation
+ """Represents an annotation on a document.
+
+ Annotations are key/value pairs over a range of content. Annotations
+ can be used to store data or to be interpreted by a client when displaying
+ the data.
+ """
+ attr_reader :range, :name, :value
+
+ JAVA_CLASS = 'com.google.wave.api.Annotation'
+
+ def initialize(name, value, r=nil)
+ """Initializes this annotation with a name and value pair and a range.
+
+ Args:
+ name: Key name for this annotation.
+ value: Value of this annotation.
+ r: Range that this annotation is valid over.
+ """
+ @name = name
+ @value = value
+ @range = r || Range.new(0,1)
+ end
+end
+
+class StringEnum
+ """Enum like class that is configured with a list of values.
+
+ This class effectively implements an enum for Elements, except for that
+ the actual values of the enums will be the string values."""
+
+ def self.enum(*values)
+ values.each {|name| const_set(name.to_sym, name)}
+ end
+end
+
+
+class ELEMENT_TYPE < StringEnum
+ enum 'INLINE_BLIP', 'INPUT', 'CHECK', 'LABEL', 'BUTTON'
+ enum 'RADIO_BUTTON', 'RADIO_BUTTON_GROUP','PASSWORD', 'GADGET', 'IMAGE'
+end
+
+
+class Element
+ """Elements are non-text content within a document.
+
+ These are generally abstracted from the Robot. Although a Robot can query the
+ properties of an element it can only interact with the specific types that
+ the element represents.
+
+ Properties of elements are both accesible directly (image.url) and through
+ the properties dictionary (image.properties['url']). In general Element
+ should not be instantiated by robots, but rather rely on the derrived classes.
+ """
+ attr_accessor :type
+
+ JAVA_CLASS = 'com.google.wave.api.Element'
+
+ def initialize(element_type, properties)
+ """Initializes self with the specified type and any properties."""
+ @type = element_type
+ properties.each_pair do |key, val|
+ eval <<-EOD
+ def #{key}
+ @#{key}
+ end
+ def #{key}=(value)
+ @#{key}=value
+ end
+ @#{key}=val
+ EOD
+ end
+ end
+
+ def Serialize()
+ """Custom serializer for Elements.
+
+ Element need their non standard attributes returned in a dict named
+ properties.
+ """
+ props = {}
+ data = {}
+ for attr in self.methods
+ next if Object.respond_to? attr
+ next unless method(attr).arity == 0
+ next if attr == 'Serialize'
+ val = send(attr)
+ next if val == self
+ next if val == nil
+ val = Util.Serialize(val)
+ props[attr] = val
+ end
+ data['java_class'] = self.class::JAVA_CLASS
+ data['type'] = type
+ data['properties'] = Util.Serialize(props)
+ return data
+ end
+end
+
+
+class FormElement < Element
+
+ JAVA_CLASS = 'com.google.wave.api.FormElement'
+
+ def initialize(element_type, name, properties={})
+ defaults = {:name=>name, :value=>'', :default_value=>'', :label=>''}
+ super(element_type, defaults.merge(properties))
+ end
+end
+
+class Gadget < Element
+
+ JAVA_CLASS = 'com.google.wave.api.Gadget'
+
+ def initialize(url='')
+ super(ELEMENT_TYPE::GADGET, :url=>url)
+ end
+end
+
+class Image < Element
+
+ JAVA_CLASS = 'com.google.wave.api.Image'
+
+ def initialize(url='', properties={})
+ defaults = {:url=>url, :width=>nil, :height=>nil,
+ :attachment_id=>nil, :caption=>nil}
+ super(ELEMENT_TYPE::IMAGE, defaults.merge(properties))
+ end
+end
105 src/waveapi/document_test.rb
@@ -0,0 +1,105 @@
+#!/usr/bin/ruby
+#
+# Copyright 2009 Google Inc. All Rights Reserved.
+
+"""Unit tests for the document module."""
+
+
+__author__ = 'davidbyttow@google.com (David Byttow)'
+
+
+require 'test/unit'
+
+require 'document'
+require 'util'
+
+
+# class TestRange < Test::Unit::TestCase
+# """Tests for the Range class."""
+#
+# def testDefaults()
+# r = Range.new()
+# assert_equal(0, r.first)
+# assert_equal(1, r.last)
+# end
+#
+# def testValidRanges()
+# r = Range.new(1, 2)
+# assert_equal(1, r.first)
+# assert_equal(2, r.last)
+# end
+#
+# def testInvalidRanges()
+# self.assertRaises(ValueError, Range, 1, 0)
+# self.assertRaises(ValueError, Range, 0, -1)
+# self.assertRaises(ValueError, Range, 3, 1)
+# end
+#
+# def testCollapsedRanges()
+# self.assertTrue(Range.new(0, 0).IsCollapsed())
+# self.assertTrue(Range.new(1, 1).IsCollapsed())
+# end
+#end
+
+class TestAnnotation < Test::Unit::TestCase
+ """Tests for the Annotation class."""
+
+ def testDefaults()
+ annotation = Annotation.new('key', 'value')
+ assert_equal(Range.new(0,1).first, annotation.range.first)
+ assert_equal(Range.new(0,1).last, annotation.range.last)
+ end
+
+ def testFields()
+ annotation = Annotation.new('key', 'value', Range.new(2, 3))
+ assert_equal('key', annotation.name)
+ assert_equal('value', annotation.value)
+ assert_equal(2, annotation.range.first)
+ assert_equal(3, annotation.range.last)
+ end
+end
+
+class TestElement < Test::Unit::TestCase
+ """Tests for the Element class."""
+
+ def testProperties()
+ element = Element.new(ELEMENT_TYPE::GADGET, :key=>'value')
+ assert_equal('value', element.key)
+ end
+
+ def testFormElement()
+ element = FormElement.new(ELEMENT_TYPE::INPUT, 'input', :label=>'label')
+ assert_equal(ELEMENT_TYPE::INPUT, element.type)
+ assert_equal(element.value, '')
+ assert_equal(element.name, 'input')
+ assert_equal(element.label, 'label')
+ end
+
+ def testImage()
+ image = Image.new('http://test.com/image.png', :width=>100, :height=>100)
+ assert_equal(ELEMENT_TYPE::IMAGE, image.type)
+ assert_equal(image.url, 'http://test.com/image.png')
+ assert_equal(image.width, 100)
+ assert_equal(image.height, 100)
+ end
+
+ def testGadget()
+ gadget = Gadget.new('http://test.com/gadget.xml')
+ assert_equal(ELEMENT_TYPE::GADGET, gadget.type)
+ assert_equal(gadget.url, 'http://test.com/gadget.xml')
+ end
+
+ def testSerialize()
+ image = Image.new('http://test.com/image.png', :width=>100, :height=>100)
+ s = Util.Serialize(image)
+ # we should really only have three things to serialize
+ assert_equal(['java_class', 'properties', 'type'], s.keys.sort)
+ assert_equal(s['java_class'], 'com.google.wave.api.Image')
+ assert_equal(s['properties']['javaClass'], 'java.util.HashMap')
+ props = s['properties']['map']
+ assert_equal(props.length, 3)
+ assert_equal(props['url'], 'http://test.com/image.png')
+ assert_equal(props['width'], 100)
+ assert_equal(props['height'], 100)
+ end
+end
15 src/waveapi/errors.rb
@@ -0,0 +1,15 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2009 Google Inc. All Rights Reserved.
+
+"""Contains various API-specific exception classes.
+
+This module contains various specific exception classes that are raised by
+the library back to the client.
+"""
+
+__author__ = 'davidbyttow@google.com (David Byttow)'
+
+
+class Error(Exception):
+ """Base library error type."""
27 src/waveapi/events.rb
@@ -0,0 +1,27 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2009 Google Inc. All Rights Reserved.
+
+"""Defines event types that are sent from the wave server.
+
+This module defines all of the event types currently supported by the wave
+server.
+"""
+
+__author__ = 'davidbyttow@google.com (David Byttow)'
+
+
+# Event Types
+WAVELET_BLIP_CREATED = 'WAVELET_BLIP_CREATED'
+WAVELET_BLIP_REMOVED = 'WAVELET_BLIP_REMOVED'
+WAVELET_PARTICIPANTS_CHANGED = 'WAVELET_PARTICIPANTS_CHANGED'
+WAVELET_TIMESTAMP_CHANGED = 'WAVELET_TIMESTAMP_CHANGED'
+WAVELET_TITLE_CHANGED = 'WAVELET_TITLE_CHANGED'
+WAVELET_VERSION_CHANGED = 'WAVELET_VERSION_CHANGED'
+BLIP_CONTRIBUTORS_CHANGED = 'BLIP_CONTRIBUTORS_CHANGED'
+BLIP_DELETED = 'BLIP_DELETED'
+BLIP_SUBMITTED = 'BLIP_SUBMITTED'
+BLIP_TIMESTAMP_CHANGED = 'BLIP_TIMESTAMP_CHANGED'
+BLIP_VERSION_CHANGED = 'BLIP_VERSION_CHANGED'
+DOCUMENT_CHANGED = 'DOCUMENT_CHANGED'
+FORM_BUTTON_CLICKED = 'FORM_BUTTON_CLICKED'
400 src/waveapi/model.rb
@@ -0,0 +1,400 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2009 Google Inc. All Rights Reserved.
+
+"""Defines classes that represent parts of the common wave model.
+
+Defines the core data structures for the common wave model. At this level,
+models are read-only but can be modified through operations.
+"""
+
+__author__ = 'davidbyttow@google.com (David Byttow)'
+
+
+require 'document'
+require 'logger'
+
+module Model
+ ROOT_WAVELET_ID_SUFFIX = '!conv+root'
+
+
+ class WaveData
+ """Defines the data for a single wave."""
+ attr_accessor :id, :wavelet_ids
+
+ def initialize()
+ @id = nil
+ @wavelet_ids = []
+ end
+ end
+
+ class Wave
+ """Models a single wave instance.
+
+ A single wave is composed of its id and any wavelet ids that belong to it.
+ """
+
+ attr_reader :_data
+
+ def initialize(data)
+ """Inits this wave with its data.
+
+ Args:
+ data: A WaveData instance.
+ """
+
+ @_data = data
+ end
+
+ def GetId()
+ """Returns this wave's id."""
+ return @_data.id
+ end
+
+ def GetWaveletIds()
+ """Returns a set of wavelet ids."""
+ return @_data.wavelet_ids
+ end
+ end
+
+ class WaveletData
+ """Defines the data for a single wavelet."""
+
+ JAVA_CLASS = 'com.google.wave.api.impl.WaveletData'
+ attr_accessor :creator, :creation_time, :data_documents
+ attr_accessor :last_modified_time, :participants, :root_blip_id
+ attr_accessor :title, :version, :wave_id, :wavelet_id
+
+ def initialize()
+ @creator = nil
+ @creation_time = 0
+ @data_documents = {}
+ @last_modified_time = 0
+ @participants = []
+ @root_blip_id = nil
+ @title = ''
+ @version = 0
+ @wave_id = nil
+ @wavelet_id = nil
+ end
+ end
+
+ class Wavelet
+ """Models a single wavelet instance.
+
+ A single wavelet is composed of metadata, participants and the blips it
+ contains.
+ """
+
+ attr_reader :_data
+
+ def initialize(data)
+ """Inits this wavelet with its data.
+
+ Args:
+ data: A WaveletData instance.
+ """
+ @_data = data
+ end
+
+ def GetCreator()
+ """Returns the participant id of the creator of this wavelet."""
+ return @_data.creator
+ end
+
+ def GetCreationTime()
+ """Returns the time that this wavelet was first created in milliseconds."""
+ return @_data.creation_time
+ end
+
+ def GetDataDocument(name, default=nil)
+ """Returns a data document for this wavelet based on key name."""
+ return @_data.data_documents.fetch(name, default)
+ end
+
+ def GetId()
+ """Returns this wavelet's id."""
+ return @_data.wavelet_id
+ end
+
+ def GetLastModifiedTime()
+ """Returns the time that this wavelet was last modified in milliseconds."""
+ return @_data.last_modified_time
+ end
+
+ def GetParticipants()
+ """Returns a set of participants on this wavelet."""
+ return @_data.participants
+ end
+
+ def GetRootBlipId()
+ """Returns this wavelet's root blip id."""
+ return @_data.root_blip_id
+ end
+
+ def GetTitle()
+ """Returns the title of this wavelet."""
+ return @_data.title
+ end
+
+ def GetWaveId()
+ """Returns this wavelet's parent wave id."""
+ return @_data.wave_id
+ end
+ end
+
+ class BlipData
+ """Data that describes a single blip."""
+
+ JAVA_CLASS = 'com.google.wave.api.impl.BlipData'
+ attr_accessor :annotations, :child_blip_ids, :content, :contributors
+ attr_accessor :creator, :elements, :last_modified_time, :parent_blip_id
+ attr_accessor :blip_id, :version, :wave_id, :wavelet_id
+
+ def initialize()
+ @annotations = []
+ @blip_id = nil
+ @child_blip_ids = []
+ @content = ''
+ @contributors = []
+ @creator = nil
+ @elements = {}
+ @last_modified_time = 0
+ @parent_blip_id = nil
+ @version = -1
+ @wave_id = nil
+ @wavelet_id = nil
+ end
+ end
+
+ class Blip
+ """Models a single blip instance.
+
+ Blips are essentially elements of conversation. Blips can live in a
+ hierarchy of blips. A root blip has no parent blip id, but all blips
+ have the ids of the wave and wavelet that they are associated with.
+
+ Blips also contain annotations, content and elements, which are accessed via
+ the Document object.
+ """
+
+ def initialize(data, doc)
+ """Inits this blip with its data and document view.
+
+ Args:
+ data: A BlipData instance.
+ doc: A Document instance associated with this blip.
+ """
+ @_data = data
+ @_document = doc
+ end
+
+ def GetChildBlipIds()
+ """Returns a set of blip ids that are children of this blip."""
+ return @_data.child_blip_ids
+ end
+
+ def GetContributors()
+ """Returns a set of participant ids that contributed to this blip."""
+ return @_data.contributors
+ end
+
+ def GetCreator()
+ """Returns the id of the participant that created this blip."""
+ return @_data.creator
+ end
+
+ def GetDocument()
+ """Returns the Document of this blip, which contains content data."""
+ return @_document
+ end
+
+ def GetId()
+ """Returns the id of this blip."""
+ return @_data.blip_id
+ end
+
+ def GetLastModifiedTime()
+ """Returns the time that this blip was last modified by the server."""
+ return @_data.last_modified_time
+ end
+
+ def GetParentBlipId()
+ """Returns the id of this blips parent or nil if it is the root."""
+ return @_data.parent_blip_id
+ end
+
+ def GetWaveId()
+ """Returns the id of the wave that this blip belongs to."""
+ return @_data.wave_id
+ end
+
+ def GetWaveletId()
+ """Returns the id of the wavelet that this blip belongs to."""
+ return @_data.wavelet_id
+ end
+
+ def IsRoot()
+ """Returns True if this is the root blip of a wavelet."""
+ return @_data.parent_blip_id == nil
+ end
+ end
+
+ class Document
+ """Base representation of a document of a blip.
+
+ TODO(davidbyttow): Add support for annotations and elements.
+ """
+
+ def initialize(blip_data)
+ """Inits this document with the data of the blip it is representing.
+
+ Args:
+ blip_data: A BlipData instance.
+ """
+ @_blip_data = blip_data
+ end
+
+ def GetText()
+ """Returns the raw text content of this document."""
+ return @_blip_data.content
+ end
+ end
+
+ class Event
+ """Data describing a single event."""
+ attr_accessor :type, :timestamp, :modified_by, :properties
+
+ def initialize()
+ @type = ''
+ @timestamp = 0
+ @modified_by = ''
+ @properties = {}
+ end
+ end
+
+ def self.CreateEvent(data)
+ """Construct event data from the raw incoming wire protocol."""
+ event = Event.new()
+ event.type = data['type']
+ event.timestamp = data['timestamp']
+ event.modified_by = data['modifiedBy']
+ event.properties = data['properties'] or {}
+ return event
+ end
+
+ def self.CreateWaveletData(data)
+ """Construct wavelet data from the raw incoming wire protocol.
+
+ TODO(davidbyttow): Automate this based on naming like the Serialize methods.
+
+ Args:
+ data: Serialized data from server.
+
+ Returns:
+ Instance of WaveletData based on the fields.
+ """
+ wavelet_data = WaveletData.new()
+ wavelet_data.creator = data['creator']
+ wavelet_data.creation_time = data['creationTime']
+ wavelet_data.data_documents = data['dataDocuments'] or {}
+ wavelet_data.last_modified_time = data['lastModifiedTime']
+ wavelet_data.participants = data['participants']
+ wavelet_data.root_blip_id = data['rootBlipId']
+ wavelet_data.title = data['title']
+ wavelet_data.version = data['version']
+ wavelet_data.wave_id = data['waveId']
+ wavelet_data.wavelet_id = data['waveletId']
+ return wavelet_data
+ end
+
+ def self.CreateBlipData(data)
+ """Construct blip data from the raw incoming wire protocol.
+
+ TODO(davidbyttow): Automate this based on naming like the Serialize methods.
+
+ Args:
+ data: Serialized data from server.
+
+ Returns:
+ Instance of BlipData based on the fields.
+ """
+ blip_data = BlipData.new()
+ blip_data.annotations = []
+ for annotation in data['annotations']
+ r = Range.new(annotation['range']['start'], annotation['range']['end'])
+ blip_data.annotations.push(Annotation.new(annotation['name'],
+ annotation['value'],
+ :r=>r))
+ end
+ blip_data.child_blip_ids = data['childBlipIds']
+ blip_data.content = data['content']
+ blip_data.contributors = data['contributors']
+ blip_data.creator = data['creator']
+ blip_data.elements = data['elements']
+ blip_data.last_modified_time = data['lastModifiedTime']
+ blip_data.parent_blip_id = data['parentBlipId']
+ blip_data.blip_id = data['blipId']
+ blip_data.version = data['version']
+ blip_data.wave_id = data['waveId']
+ blip_data.wavelet_id = data['waveletId']
+ return blip_data
+ end
+
+ class Context
+ """Contains information associated with a single request from the server.
+
+ This includes the current waves in this session
+ and any operations that have been enqueued during request processing.
+ """
+
+ def initialize()
+ @_waves = {}
+ @_wavelets = {}
+ @_blips = {}
+ @_operations = []
+ end
+
+ def GetBlipById(blip_id)
+ """Returns a blip by id or nil if it does not exist."""
+ return @_blips[blip_id]
+ end
+
+ def GetWaveById(wave_id)
+ """Returns a wave by id or nil if it does not exist."""
+ return @_waves[wave_id]
+ end
+
+ def GetWaveletById(wavelet_id)
+ """Returns a wavelet by id or nil if it does not exist."""
+ return @_wavelets[wavelet_id]
+ end
+
+ def GetRootWavelet()
+ """Returns the root wavelet or nil if it is not in this context."""
+ for wavelet in @_wavelets.values()
+ wavelet_id = wavelet.GetId()
+ if wavelet_id.endswith(ROOT_WAVELET_ID_SUFFIX)
+ return wavelet
+ end
+ end
+ logging.warning("Could not retrieve root wavelet.")
+ return nil
+ end
+
+ def GetWaves()
+ """Returns the list of waves associated with this session."""
+ return @_waves.values()
+ end
+
+ def GetWavelets()
+ """Returns the list of wavelets associated with this session."""
+ return @_wavelets.values()
+ end
+
+ def GetBlips()
+ """Returns the list of blips associated with this session."""
+ return @_blips.values()
+ end
+ end
+end
95 src/waveapi/model_test.rb
@@ -0,0 +1,95 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2009 Google Inc. All Rights Reserved.
+
+"""Unit tests for the model module."""
+
+
+__author__ = 'davidbyttow@google.com (David Byttow)'
+
+
+require 'test/unit'
+
+require 'model'
+
+
+class TestWaveModel < Test::Unit::TestCase
+ """Tests the primary data structures for the wave model."""
+
+ def setup()
+ wave_data = Model::WaveData.new()
+ wave_data.id = 'my-wave'
+ wave_data.wavelet_ids = ['wavelet-1']
+ @test_wave_data = wave_data
+
+ wavelet_data = Model::WaveletData.new()
+ wavelet_data.creator = 'creator@google.com'
+ wavelet_data.creation_time = 100
+ wavelet_data.last_modified_time = 101
+ wavelet_data.participants = ['robot@google.com']
+ wavelet_data.root_blip_id = 'blip-1'
+ wavelet_data.wave_id = wave_data.id
+ wavelet_data.wavelet_id = 'wavelet-1'
+ @test_wavelet_data = wavelet_data
+
+ blip_data = Model::BlipData.new()
+ blip_data.blip_id = wavelet_data.root_blip_id
+ blip_data.content = '<p>testing</p>'
+ blip_data.contributors = [wavelet_data.creator, 'robot@google.com']
+ blip_data.creator = wavelet_data.creator
+ blip_data.last_modified_time = wavelet_data.last_modified_time
+ blip_data.parent_blip_id = nil
+ blip_data.wave_id = wave_data.id
+ blip_data.wavelet_id = wavelet_data.wavelet_id
+ @test_blip_data = blip_data
+ end
+
+ def testWaveFields()
+ w = Model::Wave.new(@test_wave_data)
+ assert_equal(@test_wave_data.id, w.GetId())
+ assert_equal(@test_wave_data.wavelet_ids, w.GetWaveletIds())
+ end
+
+ def testWaveletFields()
+ w = Model::Wavelet.new(@test_wavelet_data)
+ assert_equal(@test_wavelet_data.creator, w.GetCreator())
+ end
+
+ def testBlipFields()
+ b = Model::Blip.new(@test_blip_data, Model::Document.new(@test_blip_data))
+ assert_equal(@test_blip_data.child_blip_ids,
+ b.GetChildBlipIds())
+ assert_equal(@test_blip_data.contributors, b.GetContributors())
+ assert_equal(@test_blip_data.creator, b.GetCreator())
+ assert_equal(@test_blip_data.content,
+ b.GetDocument().GetText())
+ assert_equal(@test_blip_data.blip_id, b.GetId())
+ assert_equal(@test_blip_data.last_modified_time,
+ b.GetLastModifiedTime())
+ assert_equal(@test_blip_data.parent_blip_id,
+ b.GetParentBlipId())
+ assert_equal(@test_blip_data.wave_id,
+ b.GetWaveId())
+ assert_equal(@test_blip_data.wavelet_id,
+ b.GetWaveletId())
+ assert_equal(true, b.IsRoot())
+ end
+
+ def testBlipIsRoot()
+ @test_blip_data.parent_blip_id = 'blip-parent'
+ b = Model::Blip.new(@test_blip_data, Model::Document.new(@test_blip_data))
+ assert_equal(false, b.IsRoot())
+ end
+
+ def testCreateEvent()
+ data = {'type' => 'WAVELET_PARTICIPANTS_CHANGED',
+ 'properties' => {'blipId' => 'blip-1'},
+ 'timestamp' => 123,
+ 'modifiedBy' => 'modifier@google.com'}
+ event_data = Model.CreateEvent(data)
+ assert_equal(data['type'], event_data.type)
+ assert_equal(data['properties'], event_data.properties)
+ assert_equal(data['timestamp'], event_data.timestamp)
+ assert_equal(data['modifiedBy'], event_data.modified_by)
+ end
+end
37 src/waveapi/module_test_runner.rb
@@ -0,0 +1,37 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2009 Google Inc. All Rights Reserved.
+
+"""Module defines the ModuleTestRunnerClass."""
+
+
+__author__ = 'davidbyttow@google.com (David Byttow)'
+
+
+require 'test/unit'
+
+
+class ModuleTestRunner
+ """Responsible for executing all test cases in a list of modules."""
+
+ def initialize(module_list=None, module_test_settings=None)
+ self.modules = module_list or []
+ self.settings = module_test_settings or {}
+ end
+
+ def RunAllTests
+ """Executes all tests present in the list of modules."""
+ runner = unittest.TextTestRunner()
+ for module_name in @modules
+ for setting, value in self.settings.iteritems()
+ begin
+ setattr(module_name, setting, value)
+ rescue AttributeError:
+ print '\nError running ' + str(setting)
+ end
+ end
+ print '\nRunning all tests in module', module_name.__name__
+ runner.run(unittest.defaultTestLoader.loadTestsFromModule(module_name))
+ end
+ end
+end
1,045 src/waveapi/ops.rb
@@ -0,0 +1,1045 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2009 Google Inc. All Rights Reserved.
+
+"""Support for operations that can be applied to the server.
+
+Contains classes and utilities for creating operations that are to be
+applied on the server.
+"""
+
+__author__ = 'davidbyttow@google.com (David Byttow)'
+
+
+require 'document'
+require 'model'
+require 'util'
+
+# Operation Types
+WAVELET_APPEND_BLIP = 'WAVELET_APPEND_BLIP'
+WAVELET_ADD_PARTICIPANT = 'WAVELET_ADD_PARTICIPANT'
+WAVELET_CREATE = 'WAVELET_CREATE'
+WAVELET_REMOVE_SELF = 'WAVELET_REMOVE_SELF'
+WAVELET_DATADOC_SET = 'WAVELET_DATADOC_SET'
+WAVELET_SET_TITLE = 'WAVELET_SET_TITLE'
+BLIP_CREATE_CHILD = 'BLIP_CREATE_CHILD'
+BLIP_DELETE = 'BLIP_DELETE'
+DOCUMENT_ANNOTATION_DELETE = 'DOCUMENT_ANNOTATION_DELETE'
+DOCUMENT_ANNOTATION_SET = 'DOCUMENT_ANNOTATION_SET'
+DOCUMENT_ANNOTATION_SET_NORANGE = 'DOCUMENT_ANNOTATION_SET_NORANGE'
+DOCUMENT_APPEND = 'DOCUMENT_APPEND'
+DOCUMENT_APPEND_STYLED_TEXT = 'DOCUMENT_APPEND_STYLED_TEXT'
+DOCUMENT_INSERT = 'DOCUMENT_INSERT'
+DOCUMENT_DELETE = 'DOCUMENT_DELETE'
+DOCUMENT_REPLACE = 'DOCUMENT_REPLACE'
+DOCUMENT_ELEMENT_APPEND = 'DOCUMENT_ELEMENT_APPEND'
+DOCUMENT_ELEMENT_DELETE = 'DOCUMENT_ELEMENT_DELETE'
+DOCUMENT_ELEMENT_INSERT = 'DOCUMENT_ELEMENT_INSERT'
+DOCUMENT_ELEMENT_INSERT_AFTER = 'DOCUMENT_ELEMENT_INSERT_AFTER'
+DOCUMENT_ELEMENT_INSERT_BEFORE = 'DOCUMENT_ELEMENT_INSERT_BEFORE'
+DOCUMENT_ELEMENT_REPLACE = 'DOCUMENT_ELEMENT_REPLACE'
+DOCUMENT_INLINE_BLIP_APPEND = 'DOCUMENT_INLINE_BLIP_APPEND'
+DOCUMENT_INLINE_BLIP_DELETE = 'DOCUMENT_INLINE_BLIP_DELETE'
+DOCUMENT_INLINE_BLIP_INSERT = 'DOCUMENT_INLINE_BLIP_INSERT'
+DOCUMENT_INLINE_BLIP_INSERT_AFTER_ELEMENT = ('DOCUMENT_INLINE_BLIP_INSERT_'
+ 'AFTER_ELEMENT')
+
+
+class Operation
+ """Represents a generic operation applied on the server.
+
+ This operation class contains data that is filled in depending on the
+ operation type.
+
+ It can be used directly, but doing so will not result
+ in local, transient reflection of state on the blips. In other words,
+ creating a 'delete blip' operation will not remove the blip from the local
+ context for the duration of this session. It is better to use the OpBased
+ model classes directly instead.
+ """
+
+ JAVA_CLASS = 'com.google.wave.api.impl.OperationImpl'
+ attr_accessor :type, :wave_id, :wavelet_id, :blip_id, :index, :property
+
+ def initialize(op_type, wave_id, wavelet_id, blip_id='', index=-1,
+ prop=nil)
+ """Initializes this operation with contextual data.
+
+ Args:
+ op_type: Type of operation.
+ wave_id: The id of the wave that this operation is to be applied.
+ wavelet_id: The id of the wavelet that this operation is to be applied.
+ blip_id: The optional id of the blip that this operation is to be applied.
+ index: Optional integer index for content-based operations.
+ prop: A weakly typed property object is based on the context of this
+ operation.
+ """
+ self.type = op_type
+ self.wave_id = wave_id
+ self.wavelet_id = wavelet_id
+ self.blip_id = blip_id
+ self.index = index
+ self.property = prop
+ end
+end
+
+class OpBasedWave < Model::Wave
+ """Subclass of the wave model capable of generating operations.
+
+ Any mutation-based methods will likely result in one or more operations
+ being applied locally and sent to the server.
+ """
+
+ def initialize(data, context)
+ """Initializes this wave with the session context."""
+ super(data)
+ @__context = context
+ end
+
+ def CreateWavelet()
+ """Creates a new wavelet on this wave."""
+ @__context.builder.WaveletCreate(self.GetId())
+ end
+end
+
+class OpBasedWavelet < Model::Wavelet
+ """Subclass of the wavelet model capable of generating operations.
+
+ Any mutation-based methods will likely result in one or more operations
+ being applied locally and sent to the server.
+ """
+
+ def initialize(data, context)
+ """Initializes this wavelet with the session context."""
+ super(data)
+ @__context = context
+ end
+
+ def CreateBlip()
+ """Creates and appends a blip to this wavelet and returns it.
+
+ Returns:
+ A transient version of the blip that was created.
+ """
+ blip_data = @__context.builder.WaveletAppendBlip(self.GetWaveId(),
+ self.GetId())
+ return @__context.AddBlip(blip_data)
+ end
+
+ def AddParticipant(participant_id)
+ """Adds a participant to a wavelet.
+
+ Args:
+ participant_id: Id of the participant that is to be added.
+ """
+ @__context.builder.WaveletAddParticipant(self.GetWaveId(), self.GetId(),
+ participant_id)
+ @_data.participants.push(participant_id) unless
+ @_data.participants.include?(participant_id)
+ end
+
+ def RemoveSelf
+ """Removes this robot from the wavelet."""
+ @__context.builder.WaveletRemoveSelf(self.GetWaveId(), self.GetId())
+ # TODO(davidbyttow): Locally remove the robot.
+ end
+
+ def SetDataDocument(name, data)
+ """Sets a key/value pair on the wavelet data document.
+
+ Args:
+ name: The string key.
+ data: The value associated with this key.
+ """
+ @__context.builder.WaveletSetDataDoc(self.GetWaveId(), self.GetId(),
+ name, data)
+ @_data.data_documents[name] = data
+ end
+
+ def SetTitle(title)
+ """Sets the title of this wavelet.
+
+ Args:
+ title: String title to for this wave.
+ """
+ @__context.builder.WaveletSetTitle(self.GetWaveId(), self.GetId(),
+ title)
+ @__data.title = title
+ end
+end
+
+class OpBasedBlip < Model::Blip
+ """Subclass of the blip model capable of generating operations.
+
+ Any mutation-based methods will likely result in one or more operations
+ being applied locally and sent to the server.
+ """
+ attr_accessor :_data, :_document
+
+ def initialize(data, context)
+ """Initializes this blip with the session context."""
+ super(data, OpBasedDocument.new(data, context))
+ @__context = context
+ end
+
+ def CreateChild
+ """Creates a child blip of this blip."""
+ blip_data = @__context.builder.BlipCreateChild(self.GetWaveId(),
+ self.GetWaveletId(),
+ self.GetId())
+ return @__context.AddBlip(blip_data)
+ end
+
+ def Delete
+ """Deletes this blip from the wavelet."""
+ @__context.builder.BlipDelete(self.GetWaveId(),
+ self.GetWaveletId(),
+ self.GetId())
+ return @__context.RemoveBlip(self.GetId())
+ end
+end
+
+class OpBasedDocument < Model::Document
+ """Subclass of the document model capable of generating operations.
+
+ Any mutation-based methods will likely result in one or more operations
+ being applied locally and sent to the server.
+
+ TODO(davidbyttow): Manage annotations and elements as content is updated.
+ """
+
+ def initialize(blip_data, context)
+ """Initializes this document with its owning blip and session context."""
+ super(blip_data)
+ @__context = context
+ end
+
+ def HasAnnotation(name)
+ """Determines if given named annotation is anywhere on this document.
+
+ Args:
+ name: The key name of the annotation.
+
+ Returns:
+ True if the annotation exists.
+ """
+ for annotation in @_blip_data.annotations
+ if annotation.name == name
+ return true
+ end
+ end
+ return false
+ end
+
+ def SetText(text)
+ """Clears and sets the text of this document.
+
+ Args:
+ text: The text content to replace this document with.
+ """
+ self.Clear()
+ @__context.builder.DocumentInsert(@_blip_data.wave_id,
+ @_blip_data.wavelet_id,
+ @_blip_data.blip_id,
+ text)
+ @_blip_data.content = text
+ end
+
+ def SetTextInRange(r, text)
+ """Deletes text within a range and sets the supplied text in its place.
+
+ Args:
+ r: Range to delete and where to set the new text.
+ text: The text to set at the range start position.
+ """
+ self.DeleteRange(r)
+ self.InsertText(r.first, text)
+ end
+
+ def InsertText(start, text)
+ """Inserts text at a specific position.
+
+ Args:
+ start: The index position where to set the text.
+ text: The text to set.
+ """
+ @__context.builder.DocumentInsert(@_blip_data.wave_id,
+ @_blip_data.wavelet_id,
+ @_blip_data.blip_id,
+ text, index=start)
+ left = @_blip_data.content[0...start]
+ right = @_blip_data.content[start..-1]
+ @_blip_data.content = left + text + right
+ end
+
+ def AppendText(text)
+ """Appends text to the end of this document.
+
+ Args:
+ text: The text to append.
+ """
+ @__context.builder.DocumentAppend(@_blip_data.wave_id,
+ @_blip_data.wavelet_id,
+ @_blip_data.blip_id,
+ text)
+ @_blip_data.content += text
+ end
+
+ def Clear
+ """Clears the content of this document."""
+ @__context.builder.DocumentDelete(@_blip_data.wave_id,
+ @_blip_data.wavelet_id,
+ @_blip_data.blip_id,
+ 0, @_blip_data.content.length)
+ @_blip_data.content = ''
+ end
+
+ def DeleteRange(r)
+ """Deletes the content in the specified range.
+
+ Args:
+ r: A Range instance specifying the range to delete.
+ """
+ @__context.builder.DocumentDelete(@_blip_data.wave_id,
+ @_blip_data.wavelet_id,
+ @_blip_data.blip_id,
+ r.first, r.end)
+ left = @_blip_data.content[0...r.first]
+ right = @_blip_data.content[r.end + 1..-1]
+ @_blip_data.content = left + right
+ end
+
+ def AnnotateDocument(name, value)
+ """Annotates the entire document.
+
+ Args:
+ name: A string as the key for this annotation.
+ value: The value of this annotation.
+ """
+ b = @__context.builder
+ b.DocumentAnnotationSetNoRange(@_blip_data.wave_id,
+ @_blip_data.wavelet_id,
+ @_blip_data.blip_id,
+ name, value)
+ r = Range.new(0, @_blip_data.content.length)
+ @_blip_data.annotations.push(Annotation.new(name, value, r))
+ end
+
+ def SetAnnotation(r, name, value)
+ """Sets an annotation on a given range.
+
+ Args:
+ r: A Range specifying the range to set the annotation.
+ name: A string as the key for this annotation.
+ value: The value of this annotaton.
+ """
+ @__context.builder.DocumentAnnotationSet(@_blip_data.wave_id,
+ @_blip_data.wavelet_id,
+ @_blip_data.blip_id,
+ r.first, r.end,
+ name, value)
+ @_blip_data.annotations.push(Annotation.new(name, value, r))
+ end
+
+ def DeleteAnnotationsByName(name)
+ """Deletes all annotations with a given key name.
+
+ Args:
+ name: A string as the key for the annotation to delete.
+ """
+ size = @_blip_data.content.length
+ @__context.builder.DocumentAnnotationDelete(@_blip_data.wave_id,
+ @_blip_data.wavelet_id,
+ @_blip_data.blip_id,
+ 0, size, name)
+ for index in range(len(@_blip_data.annotations))
+ annotation = @_blip_data.annotations[index]
+ if annotation.name == name
+ del @_blip_data.annotations[index]
+ end
+ end
+ end
+
+ def DeleteAnnotationsInRange(r, name)
+ """Clears all of the annotations within a given range with a given key.
+
+ Args:
+ r: A Range specifying the range to delete.
+ name: Annotation key type to clear.
+ """
+ @__context.builder.DocumentAnnotationDelete(@_blip_data.wave_id,
+ @_blip_data.wavelet_id,
+ @_blip_data.blip_id,
+ r.first, r.end,
+ name)
+ # TODO(davidbyttow): split local annotations.
+ end
+
+ def AppendInlineBlip
+ """Appends an inline blip to this blip.
+
+ Returns:
+ The local blip that was appended.
+ """
+ blip_data = @__context.builder.DocumentInlineBlipAppend(
+ @_blip_data.wave_id, @_blip_data.wavelet_id,
+ @_blip_data.blip_id)
+ return @__context.AddBlip(blip_data)
+ end
+
+ def DeleteInlineBlip(inline_blip_id)
+ """Deletes an inline blip from this blip.
+
+ Args:
+ inline_blip_id: The id of the blip to remove.
+ """
+ @__context.builder.DocumentInlineBlipDelete(@_blip_data.wave_id,
+ @_blip_data.wavelet_id,
+ @_blip_data.blip_id,
+ inline_blip_id)
+ @__context.RemoveBlip(inline_blip_id)
+ end
+
+ def InsertInlineBlip(position)
+ """Inserts an inline blip into this blip at a specific position.
+
+ Args:
+ position: Position to insert the blip at.
+
+ Returns:
+ The BlipData of the blip that was created.
+ """
+ blip_data = @__context.builder.DocumentInlineBlipInsert(
+ @_blip_data.wave_id,
+ @_blip_data.wavelet_id,
+ @_blip_data.blip_id,
+ position)
+ # TODO(davidbyttow): Add local blip element.
+ return @__context.AddBlip(blip_data)
+ end
+
+ def DeleteElement(position)
+ """Deletes an Element at a given position.
+
+ Args:
+ position: Position of the Element to delete.
+ """
+ @__context.builder.DocumentElementDelete(@_blip_data.wave_id,
+ @_blip_data.wavelet_id,
+ @_blip_data.blip_id,
+ position)
+ end
+
+ def InsertElement(position, element)
+ """Inserts an Element at a given position.
+
+ Args:
+ position: Position of the element to replace.
+ element: The Element to replace with.
+ """
+ @__context.builder.DocumentElementInsert(@_blip_data.wave_id,
+ @_blip_data.wavelet_id,
+ @_blip_data.blip_id,
+ position, element)
+ end
+
+ def ReplaceElement(position, element)
+ """Replaces an Element at a given position with a new element.
+
+ Args:
+ position: Position of the element to replace.
+ element: The Element to replace with.
+ """
+ @__context.builder.DocumentElementReplace(@_blip_data.wave_id,
+ @_blip_data.wavelet_id,
+ @_blip_data.blip_id,
+ position, element)
+ end
+
+ def AppendElement(element)
+ @__context.builder.DocumentElementAppend(@_blip_data.wave_id,
+ @_blip_data.wavelet_id,
+ @_blip_data.blip_id,
+ element)
+ end
+end
+
+class ContextImpl_ < Model::Context
+ """An internal implementation of the Context class.
+
+ This implementation of the context is capable of adding waves, wavelets
+ and blips to itself. This is useful when applying operations locally
+ in a single session. Through this, clients can access waves, wavelets and
+ blips and add operations to be applied to those objects by the server.
+
+ Operations are applied in the order that they are received. Adding
+ operations manually will not be reflected in the state of the context.
+ """
+ attr_reader :builder
+
+ def initialize
+ super
+ @builder = OpBuilder.new(self)
+ end
+
+ def AddOperation(op)
+ """Adds an operation to the list of operations to applied by the server.
+
+ After all events are handled, the operation list is sent back to the server
+ and applied in order. Adding an operation this way will have no effect
+ on the state of the context or its entities.
+
+ Args:
+ op: An instance of an Operation.
+ """
+ @_operations.push(op)
+ end
+
+ def AddWave(wave_data)
+ """Adds a transient wave based on the data supplied.
+
+ Args:
+ wave_data: An instance of WaveData describing this wave.
+
+ Returns:
+ An OpBasedWave that may have operations applied to it.
+ """
+ wave = OpBasedWave.new(wave_data, self)
+ @_waves[wave.GetId()] = wave
+ return wave
+ end
+
+ def AddWavelet(wavelet_data)
+ """Adds a transient wavelet based on the data supplied.
+
+ Args:
+ wavelet_data: An instance of WaveletData describing this wavelet.
+
+ Returns:
+ An OpBasedWavelet that may have operations applied to it.
+ """
+ wavelet = OpBasedWavelet.new(wavelet_data, self)
+ @_wavelets[wavelet.GetId()] = wavelet
+ return wavelet
+ end
+
+ def AddBlip(blip_data)
+ """Adds a transient blip based on the data supplied.
+
+ Args:
+ blip_data: An instance of BlipData describing this blip.
+
+ Returns:
+ An OpBasedBlip that may have operations applied to it.
+ """
+ blip = OpBasedBlip.new(blip_data, self)
+ @_blips[blip.GetId()] = blip
+ return blip
+ end
+
+ def RemoveWave(wave_id)
+ """Removes a wave locally."""
+ @_waves.delete wave_id
+ end
+
+ def RemoveWavelet(wavelet_id)
+ """Removes a wavelet locally."""
+ @_wavelets.delete wavelet_id
+ end
+
+ def RemoveBlip(blip_id)
+ """Removes a blip locally."""
+ @_blips.delete blip_id
+ end
+
+ def Serialize
+ """Serialize the operation bundle.
+
+ Returns:
+ Dict representing this object.
+ """
+ data = {
+ 'javaClass' => 'com.google.wave.api.impl.OperationMessageBundle',
+ 'operations' => Util.Serialize(@_operations)
+ }
+ return data
+ end
+end
+
+def CreateContext(data)
+ """Creates a Context instance from raw data supplied by the server.
+
+ Args:
+ data: Raw data decoded from JSON sent by the server.
+
+ Returns:
+ A Context instance for this session.
+ """
+ context = ContextImpl_.new()
+ for raw_blip_data in data['blips'].values()
+ blip_data = Model.CreateBlipData(raw_blip_data)
+ context.AddBlip(blip_data)
+ end
+
+ # Currently only one wavelet is sent.
+ wavelet_data = Model.CreateWaveletData(data['wavelet'])
+ context.AddWavelet(wavelet_data)
+
+ # Waves are not sent over the wire, but we can build the list based on the
+ # wave ids of the wavelets.
+ wave_wavelet_map = {}
+ wavelets = context.GetWavelets()
+ for wavelet in wavelets:
+ wave_id = wavelet.GetWaveId()
+ wavelet_id = wavelet.GetId()
+ if not wave_wavelet_map.include? wave_id
+ wave_wavelet_map[wave_id] = []
+ end
+ wave_wavelet_map[wave_id].push(wavelet_id)
+ end
+
+ wave_wavelet_map.each_pair do |wave_id, wavelet_ids|
+ wave_data = Model::WaveData.new()
+ wave_data.id = wave_id
+ wave_data.wavelet_ids = wavelet_ids
+ context.AddWave(wave_data)
+ end
+
+ return context
+end
+
+class OpBuilder
+ """Wraps all currently supportable operations as functions.
+
+ The operation builder wraps single operations as functions and generates
+ operations in-order on its context. This should only be used when the context
+ is not available on a specific entity. For example, to modify a blip that
+ does not exist in the current context, you might specify the wave, wavelet
+ and blip id to generate an operation.
+
+ Any calls to this will not reflect the local context state in any way.
+ For example, calling WaveletAppendBlip will not result in a new blip
+ being added to the local context, only an operation to be applied on the
+ server.
+ """
+
+ def initialize(context)
+ """Initializes the op builder with the context.
+
+ Args:
+ context: A Context instance to generate operations on.
+ """
+ @__context = context
+ end
+
+ def __CreateNewBlipData(wave_id, wavelet_id)
+ """Creates an ephemeral BlipData instance used for this session."""
+ blip_data = Model::BlipData.new()
+ blip_data.wave_id = wave_id
+ blip_data.wavelet_id = wavelet_id
+ blip_data.blip_id = 'TBD_' + rand().to_s.split('.')[1]
+ return blip_data
+ end
+
+ def WaveletAppendBlip(wave_id, wavelet_id)
+ """Requests to append a blip to a wavelet.
+
+ Args:
+ wave_id: The wave id owning the containing wavelet.
+ wavelet_id: The wavelet id that this blip should be appended to.
+
+ Returns:
+ A BlipData instance representing the id information of the new blip.
+ """
+ blip_data = __CreateNewBlipData(wave_id, wavelet_id)
+ op = Operation.new(WAVELET_APPEND_BLIP, wave_id, wavelet_id, -1,
+ blip_data)
+ @__context.AddOperation(op)
+ return blip_data
+ end
+
+ def WaveletAddParticipant(wave_id, wavelet_id, participant_id)
+ """Requests to add a participant to a wavelet.
+
+ Args:
+ wave_id: The wave id owning that this operation is applied to.
+ wavelet_id: The wavelet id that this operation is applied to.
+ participant_id: Id of the participant to add.
+ """
+ op = Operation.new(WAVELET_ADD_PARTICIPANT, wave_id, wavelet_id, -1,
+ participant_id)
+ @__context.AddOperation(op)
+ end
+
+ def WaveletCreate(wave_id)
+ """Requests to create a wavelet in a wave.
+
+ Not yet implemented.
+
+ Args:
+ wave_id: The wave id owning that this operation is applied to.
+
+ Raises:
+ NotImplementedError: Function not yet implemented.
+ """
+ raise NotImplementedError
+ end
+
+ def WaveletRemoveSelf(wave_id, wavelet_id)
+ """Requests to remove this robot from a wavelet.
+
+ Not yet implemented.
+
+ Args:
+ wave_id: The wave id owning that this operation is applied to.
+ wavelet_id: The wavelet id that this operation is applied to.
+
+ Raises:
+ NotImplementedError: Function not yet implemented.
+ """
+ raise NotImplementedError
+ end
+
+ def WaveletSetDataDoc(wave_id, wavelet_id, name, data)
+ """Requests set a key/value pair on the data document of a wavelet.
+
+ Args:
+ wave_id: The wave id owning that this operation is applied to.
+ wavelet_id: The wavelet id that this operation is applied to.
+ name: The key name for this data.
+ data: The value of the data to set.
+ """
+ op = Operation.new(WAVELET_DATADOC_SET, wave_id, wavelet_id, name, -1,
+ data)
+ @__context.AddOperation(op)
+ end
+
+ def WaveletSetTitle(wave_id, wavelet_id, title)
+ """Requests to set the title of a wavelet.
+
+ Not yet implemented.
+
+ Args:
+ wave_id: The wave id owning that this operation is applied to.
+ wavelet_id: The wavelet id that this operation is applied to.
+ title: The title to set.
+
+ Raises:
+ NotImplementedError: Function not yet implemented.
+ """
+ raise NotImplementedError
+ end
+
+ def BlipCreateChild(wave_id, wavelet_id, blip_id)
+ """Requests to create a child blip of another blip.
+
+ Args:
+ wave_id: The wave id owning that this operation is applied to.
+ wavelet_id: The wavelet id that this operation is applied to.
+ blip_id: The blip id that this operation is applied to.
+
+ Returns:
+ BlipData instance for which further operations can be applied.
+ """
+ blip_data = __CreateNewBlipData(wave_id, wavelet_id)
+ op = Operation.new(BLIP_CREATE_CHILD, wave_id, wavelet_id,
+ blip_id, -1, blip_data)
+ @__context.AddOperation(op)
+ return blip_data
+ end
+
+ def BlipDelete(wave_id, wavelet_id, blip_id)
+ """Requests to delete (tombstone) a blip.
+
+ Args:
+ wave_id: The wave id owning that this operation is applied to.
+ wavelet_id: The wavelet id that this operation is applied to.
+ blip_id: The blip id that this operation is applied to.
+ """
+ op = Operation.new(BLIP_DELETE, wave_id, wavelet_id, :blip_id=>blip_id)
+ @__context.AddOperation(op)
+ end
+
+ def DocumentAnnotationDelete(wave_id, wavelet_id, blip_id, start, _end,
+ name)
+ """Deletes a specified annotation of a given range with a specific key.
+
+ Not yet implemented.
+
+ Args:
+ wave_id: The wave id owning that this operation is applied to.
+ wavelet_id: The wavelet id that this operation is applied to.
+ blip_id: The blip id that this operation is applied to.
+ start: Start position of the range.
+ _end: End position of the range.
+ name: Annotation key name to clear.
+
+ Raises:
+ NotImplementedError: Function not yet implemented.
+ """
+ raise NotImplementedError
+ end
+
+ def DocumentAnnotationSet(wave_id, wavelet_id, blip_id, start, _end,
+ name, value)
+ """Set a specified annotation of a given range with a specific key.
+
+ Args:
+ wave_id: The wave id owning that this operation is applied to.
+ wavelet_id: The wavelet id that this operation is applied to.
+ blip_id: The blip id that this operation is applied to.
+ start: Start position of the range.
+ end: End position of the range.
+ name: Annotation key name to clear.
+ value: The value of the annotation across this range.
+ """
+ annotation = Annotation.new(name, value, Range.new(start, _end))
+ op = Operation.new(DOCUMENT_ANNOTATION_SET, wave_id, wavelet_id,
+ blip_id, -1, annotation)
+ @__context.AddOperation(op)
+ end
+
+ def DocumentAnnotationSetNoRange(wave_id, wavelet_id, blip_id,
+ name, value)
+ """Requests to set an annotation on an entire document.
+
+ Args:
+ wave_id: The wave id owning that this operation is applied to.
+ wavelet_id: The wavelet id that this operation is applied to.
+ blip_id: The blip id that this operation is applied to.
+ name: Annotation key name to clear.
+ value: The value of the annotation.
+ """
+ annotation = Annotation.new(name, value, nil)
+ op = Operation.new(DOCUMENT_ANNOTATION_SET_NORANGE, wave_id, wavelet_id,
+ blip_id, -1, annotation)
+ @__context.AddOperation(op)
+ end
+
+ def DocumentAppend(wave_id, wavelet_id, blip_id, content)
+ """Requests to append content to a document.
+
+ Args:
+ wave_id: The wave id owning that this operation is applied to.
+ wavelet_id: The wavelet id that this operation is applied to.
+ blip_id: The blip id that this operation is applied to.
+ content: The content to append.
+ """
+ op = Operation.new(DOCUMENT_APPEND, wave_id, wavelet_id, blip_id,
+ -1, content)
+ @__context.AddOperation(op)
+ end
+
+ def DocumentAppendStyledText(wave_id, wavelet_id, blip_id, text, style)
+ """Requests to append styled text to the document.
+
+ Not yet implemented.
+
+ Args:
+ wave_id: The wave id owning that this operation is applied to.
+ wavelet_id: The wavelet id that this operation is applied to.
+ blip_id: The blip id that this operation is applied to.
+ text: The text ot append..
+ style: The style to apply.
+
+ Raises:
+ NotImplementedError: Function not yet implemented.
+ """
+ raise NotImplementedError
+ end
+
+ def DocumentDelete(wave_id, wavelet_id, blip_id, start, _end)
+ """Requests to delete content in a given range.
+
+ Args:
+ wave_id: The wave id owning that this operation is applied to.
+ wavelet_id: The wavelet id that this operation is applied to.
+ blip_id: The blip id that this operation is applied to.
+ start: Start of the range.
+ end: End of the range.
+ """
+ range = nil
+ if start != _end
+ range = Range.new(start, _end)
+ end
+ op = Operation.new(DOCUMENT_DELETE, wave_id, wavelet_id, blip_id, -1,
+ range)
+ @__context.AddOperation(op)
+ end
+
+ def DocumentInsert(wave_id, wavelet_id, blip_id, content, index=0)
+ """Requests to insert content into a document at a specific location.
+
+ Args:
+ wave_id: The wave id owning that this operation is applied to.
+ wavelet_id: The wavelet id that this operation is applied to.
+ blip_id: The blip id that this operation is applied to.
+ content: The content to insert.
+ index: The position insert the content at in ths document.
+ """
+ op = Operation.new(DOCUMENT_INSERT, wave_id, wavelet_id, blip_id,
+ index, content)
+ @__context.AddOperation(op)
+ end
+
+ def DocumentReplace(wave_id, wavelet_id, blip_id, content)
+ """Requests to replace all content in a document.
+
+ Args:
+ wave_id: The wave id owning that this operation is applied to.
+ wavelet_id: The wavelet id that this operation is applied to.
+ blip_id: The blip id that this operation is applied to.
+ content: Content that will replace the current document.
+ """
+ op = Operation(DOCUMENT_REPLACE, wave_id, wavelet_id, blip_id,
+ prop=content)
+ @__context.AddOperation(op)
+ end
+
+ def DocumentElementAppend(wave_id, wavelet_id, blip_id, element)
+ """Requests to append an element to the document.
+
+ Args:
+ wave_id: The wave id owning that this operation is applied to.
+ wavelet_id: The wavelet id that this operation is applied to.
+ blip_id: The blip id that this operation is applied to.
+ element: Element instance to append.
+ """
+ op = Operation.new(DOCUMENT_ELEMENT_APPEND, wave_id, wavelet_id, blip_id,
+ -1, element)
+ @__context.AddOperation(op)
+ end
+
+ def DocumentElementDelete(wave_id, wavelet_id, blip_id, position)
+ """Requests to delete an element from the document at a specific position.
+
+ Args:
+ wave_id: The wave id owning that this operation is applied to.
+ wavelet_id: The wavelet id that this operation is applied to.
+ blip_id: The blip id that this operation is applied to.
+ position: Position of the element to delete.
+ """
+ op = Operation(DOCUMENT_ELEMENT_DELETE, wave_id, wavelet_id, blip_id,
+ index=position)
+ @__context.AddOperation(op)
+ end
+
+ def DocumentElementInsert(wave_id, wavelet_id, blip_id, position,
+ element)
+ """Requests to insert an element to the document at a specific position.
+
+ Args:
+ wave_id: The wave id owning that this operation is applied to.
+ wavelet_id: The wavelet id that this operation is applied to.
+ blip_id: The blip id that this operation is applied to.
+ position: Position of the element to delete.
+ element: Element instance to insert.
+ """
+ op = Operation(DOCUMENT_ELEMENT_INSERT, wave_id, wavelet_id, blip_id,
+ index=position,
+ prop=element)
+ @__context.AddOperation(op)
+ end
+
+ def DocumentElementInsertAfter
+ """Requests to insert an element after the specified location.
+
+ Not yet implemented.
+
+ Raises:
+ NotImplementedError: Function not yet implemented.
+ """
+ raise NotImplementedError
+ end
+
+ def DocumentElementInsertBefore
+ """Requests to insert an element before the specified location.
+
+ Not yet implemented.
+
+ Raises:
+ NotImplementedError: Function not yet implemented.
+ """
+ raise NotImplementedError
+ end
+
+ def DocumentElementReplace(wave_id, wavelet_id, blip_id, position,
+ element)
+ """Requests to replace an element.
+
+ Args:
+ wave_id: The wave id owning that this operation is applied to.
+ wavelet_id: The wavelet id that this operation is applied to.
+ blip_id: The blip id that this operation is applied to.
+ position: Position of the element to replace.
+ element: Element instance to replace.
+ """
+ op = Operation(DOCUMENT_ELEMENT_REPLACE, wave_id, wavelet_id, blip_id,
+ index=position,
+ prop=element)
+ @__context.AddOperation(op)
+ end
+
+ def DocumentInlineBlipAppend(wave_id, wavelet_id, blip_id)
+ """Requests to create and append a new inline blip to another blip.
+
+ Args:
+ wave_id: The wave id owning that this operation is applied to.
+ wavelet_id: The wavelet id that this operation is applied to.
+ blip_id: The blip id that this operation is applied to.
+
+ Returns:
+ A BlipData instance containing the id information.
+ """
+ inline_blip_data = __CreateNewBlipData(wave_id, wavelet_id)
+ op = Operation.new(DOCUMENT_INLINE_BLIP_APPEND, wave_id, wavelet_id,
+ blip_id, -1, inline_blip_data)
+ @__context.AddOperation(op)
+ inline_blip_data.parent_blip_id = blip_id
+ return inline_blip_data
+ end
+
+ def DocumentInlineBlipDelete(wave_id, wavelet_id, blip_id,
+ inline_blip_id)
+ """Requests to delete an inline blip from its parent.
+
+ Args:
+ wave_id: The wave id owning that this operation is applied to.
+ wavelet_id: The wavelet id that this operation is applied to.
+ blip_id: The blip id that this operation is applied to.
+ inline_blip_id: The blip to be deleted.
+ """
+ op = Operation.new(DOCUMENT_INLINE_BLIP_DELETE, wave_id, wavelet_id,
+ blip_id, -1, inline_blip_id)
+ @__context.AddOperation(op)
+ end
+
+ def DocumentInlineBlipInsert(wave_id, wavelet_id, blip_id, position)
+ """Requests to insert an inline blip at a specific location.
+
+ Args:
+ wave_id: The wave id owning that this operation is applied to.
+ wavelet_id: The wavelet id that this operation is applied to.
+ blip_id: The blip id that this operation is applied to.
+ position: The position in the document to insert the blip.
+
+ Returns:
+ BlipData for the blip that was created for further operations.
+ """
+ inline_blip_data = __CreateNewBlipData(wave_id, wavelet_id)
+ inline_blip_data.parent_blip_id = blip_id
+ op = Operation.new(DOCUMENT_INLINE_BLIP_INSERT, wave_id, wavelet_id,
+ blip_id, position, inline_blip_data)
+ @__context.AddOperation(op)
+ return inline_blip_data
+ end
+
+ def DocumentInlineBlipInsertAfterElement
+ """Requests to insert an inline blip after an element.
+
+ Raises:
+ NotImplementedError: Function not yet implemented.
+ """
+ raise NotImplementedError
+ end
+end
264 src/waveapi/ops_test.rb
@@ -0,0 +1,264 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2009 Google Inc. All Rights Reserved.
+
+"""Unit tests for the ops module."""
+
+
+__author__ = 'davidbyttow@google.com (David Byttow)'
+
+
+require 'test/unit'
+
+require 'document'
+require 'model'
+require 'ops'
+
+
+class TestOperation < Test::Unit::TestCase
+ """Test case for Operation class."""
+
+ def testDefaults()
+ op = Operation.new(WAVELET_APPEND_BLIP, 'wave-id', 'wavelet-id')
+ assert_equal(WAVELET_APPEND_BLIP, op.type)
+ assert_equal('wave-id', op.wave_id)
+ assert_equal('wavelet-id', op.wavelet_id)
+ assert_equal('', op.blip_id)
+ assert_equal(-1, op.index)
+ assert_equal(nil, op.property)
+ end
+
+ def testFields()
+ op = Operation.new(DOCUMENT_INSERT, 'wave-id', 'wavelet-id',
+ blip_id='blip-id',
+ index=1,
+ prop='foo')
+ assert_equal(DOCUMENT_INSERT, op.type)
+ assert_equal('wave-id', op.wave_id)
+ assert_equal('wavelet-id', op.wavelet_id)
+ assert_equal('blip-id', op.blip_id)
+ assert_equal(1, op.index)
+ assert_equal('foo', op.property)
+ end
+end
+
+module TestOpBasedClasses
+ """Base class for op-based test classes. Sets up some test data."""
+
+ def setup()
+ @test_context = ContextImpl_.new()
+
+ wave_data = Model::WaveData.new()
+ wave_data.id = 'my-wave'
+ wave_data.wavelet_ids = ['wavelet-1']
+ @test_wave_data = wave_data
+ @test_wave = @test_context.AddWave(wave_data)
+
+ wavelet_data = Model::WaveletData.new()
+ wavelet_data.creator = 'creator@google.com'
+ wavelet_data.creation_time = 100
+ wavelet_data.last_modified_time = 101
+ wavelet_data.participants = ['robot@google.com']
+ wavelet_data.root_blip_id = 'blip-1'
+ wavelet_data.wave_id = wave_data.id
+ wavelet_data.wavelet_id = 'wavelet-1'
+ @test_wavelet_data = wavelet_data
+ @test_wavelet = @test_context.AddWavelet(wavelet_data)
+
+ blip_data = Model::BlipData.new()
+ blip_data.blip_id = wavelet_data.root_blip_id
+ blip_data.content = '<p>testing</p>'
+ blip_data.contributors = [wavelet_data.creator, 'robot@google.com']
+ blip_data.creator = wavelet_data.creator
+ blip_data.last_modified_time = wavelet_data.last_modified_time
+ blip_data.parent_blip_id = nil
+ blip_data.wave_id = wave_data.id
+ blip_data.wavelet_id = wavelet_data.wavelet_id
+ @test_blip_data = blip_data
+ @test_blip = @test_context.AddBlip(blip_data)
+ end
+end
+
+class TestOpBasedContext < Test::Unit::TestCase
+ """Test case for testing the operation-based context class, _ContextImpl."""
+ include TestOpBasedClasses
+
+ def testVerifySetup()
+ assert_equal(@test_wave_data,
+ @test_context.GetWaveById('my-wave')._data)
+ assert_equal(@test_wavelet_data,
+ @test_context.GetWaveletById('wavelet-1')._data)
+ assert_equal(@test_blip_data,
+ @test_context.GetBlipById('blip-1')._data)
+ end
+
+ def testRemove()
+ @test_context.RemoveWave('my-wave')
+ assert_equal(nil, @test_context.GetWaveById('my-wave'))
+ @test_context.RemoveWavelet('wavelet-1')
+ assert_equal(nil, @test_context.GetWaveletById('wavelet-1'))
+ @test_context.RemoveBlip('blip-1')
+ assert_equal(nil, @test_context.GetBlipById('blip-1'))
+ end
+end
+
+class TestOpBasedWave < Test::Unit::TestCase
+ """Test case for OpBasedWave class."""
+ include TestOpBasedClasses
+
+ def testCreateWavelet()
+ assert_raise(NotImplementedError) do
+ @test_wave.CreateWavelet
+ end
+ end
+end
+
+class TestOpBasedWavelet < Test::Unit::TestCase
+ """Test case for OpBasedWavelet class."""
+ include TestOpBasedClasses
+
+ def testCreateBlip()
+ blip = @test_wavelet.CreateBlip()
+ assert_equal('my-wave', blip.GetWaveId())
+ assert_equal('wavelet-1', blip.GetWaveletId())
+ assert_match /^TBD/, blip.GetId()
+ assert_equal(blip, @test_context.GetBlipById(blip.GetId()))
+ end
+
+ def testAddParticipant()
+ p = 'newguy@google.com'
+ @test_wavelet.AddParticipant(p)
+ assert(@test_wavelet.GetParticipants().include? p)
+ end
+
+ def testRemoveSelf()
+ assert_raise(NotImplementedError) do
+ @test_wavelet.RemoveSelf
+ end
+ end
+
+ def testSetDataDocument()
+ @test_wavelet.SetDataDocument('key', 'value')
+ assert_equal('value', @test_wavelet.GetDataDocument('key'))
+ end
+
+ def testSetTitle()
+ assert_raise(NotImplementedError) do
+ @test_wavelet.SetTitle('foo')
+ end
+ end
+end
+
+class TestOpBasedBlip < Test::Unit::TestCase
+ """Test case for OpBasedBlip class."""
+ include TestOpBasedClasses
+
+ def testCreateChild()
+ blip = @test_blip.CreateChild()
+ assert_equal('my-wave', blip.GetWaveId())
+ assert_equal('wavelet-1', blip.GetWaveletId())
+ assert_match /^TBD/, blip.GetId()
+ assert_equal(blip, @test_context.GetBlipById(blip.GetId()))
+ end
+
+ def testDelete()
+ @test_blip.Delete()
+ assert_equal(nil,
+ @test_context.GetBlipById(@test_blip.GetId()))
+ end
+end
+
+class TestOpBasedDocument < Test::Unit::TestCase
+ """Test case for OpBasedDocument class."""
+ include TestOpBasedClasses
+
+ def setup()
+ super
+ @test_doc = @test_blip.GetDocument()
+ @test_doc.SetText('123456')
+ end
+
+ def testSetText()
+ text = 'Hello test.'
+ assert(@test_doc.GetText() != text)
+ @test_doc.SetText(text)
+ assert_equal(text, @test_doc.GetText())
+ end
+
+ def testSetTextInRange()
+ text = 'abc'
+ @test_doc.SetTextInRange(Range.new(0, 2), text)
+ assert_equal('abc456', @test_doc.GetText())
+ @test_doc.SetTextInRange(Range.new(2, 2), text)
+ assert_equal('ababc456', @test_doc.GetText())
+ end
+
+ def testAppendText()
+ text = '789'
+ @test_doc.AppendText(text)
+ assert_equal('123456789', @test_doc.GetText())
+ end
+
+ def testClear()
+ @test_doc.Clear()
+ assert_equal('', @test_doc.GetText())
+ end
+
+ def testDeleteRange()
+ @test_doc.DeleteRange(Range.new(0, 1))
+ assert_equal('3456', @test_doc.GetText())
+ @test_doc.DeleteRange(Range.new(0, 0))
+ assert_equal('456', @test_doc.GetText())
+ end
+
+ def testAnnotateDocument()
+ @test_doc.AnnotateDocument('key', 'value')
+ assert(@test_doc.HasAnnotation('key'))
+ assert(!@test_doc.HasAnnotation('non-existent-key'))
+ end
+
+ def testSetAnnotation()
+ @test_doc.SetAnnotation(Range.new(0, 1), 'key', 'value')
+ assert(@test_doc.HasAnnotation('key'))
+ end
+
+ def testDeleteAnnotationByName()
+ assert_raise(NotImplementedError) do
+ @test_doc.DeleteAnnotationsByName('key')
+ end
+ end
+
+ def testDeleteAnnotationInRange()
+ assert_raise(NotImplementedError) do
+ @test_doc.DeleteAnnotationsInRange(Range.new(0, 1), 'key')
+ end
+ end
+
+ def testAppendInlineBlip()
+ blip = @test_doc.AppendInlineBlip()
+ assert_equal('my-wave', blip.GetWaveId())
+ assert_equal('wavelet-1', blip.GetWaveletId())
+ assert_match /^TBD/, blip.GetId()
+ assert_equal(@test_blip.GetId(), blip.GetParentBlipId())
+ assert_equal(blip, @test_context.GetBlipById(blip.GetId()))
+ end
+
+ def testDeleteInlineBlip()
+ blip = @test_doc.AppendInlineBlip()
+ @test_doc.DeleteInlineBlip(blip.GetId())
+ assert_equal(nil, @test_context.GetBlipById(blip.GetId()))
+ end
+
+ def testInsertInlineBlip()
+ blip = @test_doc.InsertInlineBlip(1)
+ assert_equal('my-wave', blip.GetWaveId())
+ assert_equal('wavelet-1', blip.GetWaveletId())
+ assert_match /^TBD/, blip.GetId()
+ assert_equal(@test_blip.GetId(), blip.GetParentBlipId())
+ assert_equal(blip, @test_context.GetBlipById(blip.GetId()))
+ end
+
+ def testAppendElement()
+ @test_doc.AppendElement("GADGET")
+ end
+end
121 src/waveapi/robot.rb
@@ -0,0 +1,121 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2009 Google Inc. All Rights Reserved.
+
+"""Defines the App Engine-specific robot class and associated handlers."""
+
+__author__ = 'davidbyttow@google.com (David Byttow)'
+
+
+import logging
+import traceback
+
+from google.appengine.ext import webapp
+from google.appengine.ext.webapp.util import run_wsgi_app
+
+import robot_abstract
+
+
+class RobotCapabilitiesHandler(webapp.RequestHandler):
+ """Handler for serving capabilities.xml given a robot."""
+
+ def __init__(self, robot):
+ """Initializes this handler with a specific robot."""
+ self._robot = robot
+
+ def get(self):
+ """Handles HTTP GET request."""
+ xml = self._robot.GetCapabilitiesXml()
+ self.response.headers['Content-Type'] = 'text/xml'
+ self.response.out.write(xml)
+
+
+class RobotProfileHandler(webapp.RequestHandler):
+ """Handler for serving the robot's profile information."""
+
+ def __init__(self, robot):
+ """Initializes this handler with a specific robot."""
+ self._robot = robot
+
+ def get(self):
+ """Handles HTTP GET request."""
+ self.response.headers['Content-Type'] = 'application/json'
+ self.response.out.write(self._robot.GetProfileJson())
+
+
+class RobotEventHandler(webapp.RequestHandler):
+ """Handler for the dispatching of events to various handlers to a robot.
+
+ This handler only responds to post events with a JSON post body. Its primary
+ task is to separate out the context data from the events in the post body
+ and dispatch all events in order. Once all events have been dispatched
+ it serializes the context data and its associated operations as a response.
+ """
+
+ def __init__(self, robot):
+ """Initializes self with a specific robot."""
+ self._robot = robot
+
+ def get(self):
+ """Handles the get event for debugging. Ops usually too long."""
+ ops = self.request.get('ops')
+ logging.info('get: ' + ops)
+ if ops:
+ self.request.body = ops
+ self.post()
+ self.response.headers['Content-Type'] = 'text/html'
+
+ def post(self):
+ """Handles HTTP POST requests."""
+ json_body = self.request.body
+ if not json_body:
+ # TODO(davidbyttow): Log error?
+ return
+ logging.info('Incoming: ' + json_body)
+
+ context, events = robot_abstract.ParseJSONBody(json_body)
+ for event in events:
+ try:
+ self._robot.HandleEvent(event, context)
+ except:
+ logging.error(traceback.format_exc())
+
+ json_response = robot_abstract.SerializeContext(context)
+ # Build the response.
+ logging.info('Outgoing: ' + json_response)
+ self.response.headers['Content-Type'] = 'application/json'
+ self.response.out.write(json_response)
+
+
+class Robot(robot_abstract.Robot):
+ """Adds an AppEngine setup method to the base robot class.
+
+ A robot is typically setup in the following steps:
+ 1. Instantiate and define robot.
+ 2. Register various handlers that it is interested in.
+ 3. Call Run, which will setup the handlers for the app.
+
+ For example:
+ robot = Robot('Terminator',
+ image_url='http://www.sky.net/models/t800.png',
+ profile_url='http://www.sky.net/models/t800.html')
+ robot.RegisterHandler(WAVELET_PARTICIPANTS_CHANGED, KillParticipant)