rave 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/bin/rave ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ here = File.dirname(__FILE__)
4
+ %w( create server usage war ).each do |cmd|
5
+ require File.join(here, "..", "lib", "commands", cmd)
6
+ end
7
+
8
+ args = ARGV
9
+ cmd = args.shift
10
+
11
+ if cmd
12
+ case cmd
13
+ when "create"
14
+ create_robot(args)
15
+ when "server"
16
+ start_robot(args)
17
+ when "war"
18
+ create_war(args)
19
+ else
20
+ display_usage
21
+ end
22
+ else
23
+ usage
24
+ end
@@ -0,0 +1,145 @@
1
+ require 'ftools'
2
+
3
+ #Creates a project for a robot. Args are:
4
+ # => robot_name (required)
5
+ # => image_url=http://imageurl.com/ (optional)
6
+ # => profile_url=http://profileurl.com/ (optional)
7
+ # e.g. rave my_robot image_url=http://appropriate-casey.appspot.com/image.png profile_url=http://appropriate-casey.appspot.com/profile.json
8
+ def create_robot(args)
9
+ robot_name = args.first
10
+ module_name = robot_name.split(/_|-/).collect { |word| word[0, 1].upcase + word[1, word.length-1] }.join("")
11
+ robot_class_name = "#{module_name}::Robot"
12
+ options = { :name => robot_name }
13
+ args[1, args.length-1].each do |arg|
14
+ key, value = arg.split("=").collect { |part| part.strip }
15
+ options[key.to_sym] = value
16
+ end
17
+ dir = File.join(".", robot_name)
18
+ lib = File.join(dir, "lib")
19
+ config_dir = File.join(dir, "config")
20
+ file = File.join(dir, "robot.rb")
21
+ appengine_web = File.join(dir, "appengine-web.xml")
22
+ config = File.join(dir, "config.ru")
23
+ here = File.dirname(__FILE__)
24
+ jar_dir = File.join(here, "..", "jars")
25
+ jars = %w( appengine-api-1.0-sdk-1.2.1.jar jruby-core.jar ruby-stdlib.jar )
26
+ #Create the project dir
27
+ puts "Creating directory #{File.expand_path(dir)}"
28
+ Dir.mkdir(dir)
29
+ puts "Creating robot class #{File.expand_path(file)}"
30
+ #Make the base robot class
31
+ File.open(file, "w") do |f|
32
+ f.puts robot_file_contents(module_name)
33
+ end
34
+ #Make the rackup config file
35
+ puts "Creating rackup config file #{File.expand_path(config)}"
36
+ options_str = options.collect { |key, val| ":#{key} => \"#{val}\"" }.join(", ")
37
+ File.open(config, "w") do |f|
38
+ f.puts config_file_contents(robot_class_name, options_str)
39
+ end
40
+ #Make the appengine web xml file
41
+ puts "Creating appengine config file #{File.expand_path(appengine_web)}"
42
+ File.open(appengine_web, "w") do |f|
43
+ f.puts appengine_web_contents(robot_name)
44
+ end
45
+ #Copy jars over
46
+ puts "Creating lib directory #{File.expand_path(lib)}"
47
+ Dir.mkdir(lib)
48
+ jars.each do |jar|
49
+ puts "Adding jar #{jar}"
50
+ File.copy(File.join(jar_dir, jar), File.join(lib, jar))
51
+ end
52
+ #Make the wabler config file
53
+ puts "Creating config directory #{File.expand_path(config_dir)}"
54
+ Dir.mkdir(config_dir)
55
+ warble_file = File.join(config_dir, "warble.rb")
56
+ puts "Creating warble config file #{File.expand_path(warble_file)}"
57
+ File.open(warble_file, "w") do |f|
58
+ f.puts warble_config_contents()
59
+ end
60
+ end
61
+
62
+ def robot_file_contents(module_name)
63
+ <<-ROBOT
64
+ require 'rubygems'
65
+ require 'rave'
66
+
67
+ module #{module_name}
68
+ class Robot < Rave::Models::Robot
69
+
70
+ #Define handlers here:
71
+ # e.g. if the robot should act on a DOCUMENT_CHANGED event:
72
+ #
73
+ # def document_changed(event, context)
74
+ # #Do some stuff
75
+ # end
76
+ #
77
+ # Events are:
78
+ #
79
+ # WAVELET_BLIP_CREATED, WAVELET_BLIP_REMOVED, WAVELET_PARTICIPANTS_CHANGED,
80
+ # WAVELET_TIMESTAMP_CHANGED, WAVELET_TITLE_CHANGED, WAVELET_VERSION_CHANGED,
81
+ # BLIP_CONTRIBUTORS_CHANGED, BLIP_DELETED, BLIP_SUBMITTED, BLIP_TIMESTAMP_CHANGED,
82
+ # BLIP_VERSION_CHANGED, DOCUMENT_CHANGED, FORM_BUTTON_CLICKED
83
+ #
84
+ # If you want to name your event handler something other than the default name,
85
+ # or you need to have more than one handler for an event, you can register handlers
86
+ # in the robot's constructor:
87
+ #
88
+ # def initialize(options={})
89
+ # super
90
+ # register_handler(Rave::Models::Event::DOCUMENT_CHANGED, :custom_doc_changed_handler)
91
+ # end
92
+ #
93
+ # def custom_doc_changed_handler(event, context)
94
+ # #Do some stuff
95
+ # end
96
+ #
97
+ # Note: Don't forget to call super if you define #initialize
98
+
99
+ end
100
+ end
101
+ ROBOT
102
+ end
103
+
104
+ def config_file_contents(robot_class_name, options_str)
105
+ <<-CONFIG
106
+ require 'robot'
107
+ run #{robot_class_name}.new( #{options_str} )
108
+ CONFIG
109
+ end
110
+
111
+ def appengine_web_contents(robot_name)
112
+ <<-APPENGINE
113
+ <?xml version="1.0" encoding="utf-8"?>
114
+ <appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
115
+ <application>#{robot_name}</application>
116
+ <version>1</version>
117
+ <static-files />
118
+ <resource-files />
119
+ <sessions-enabled>false</sessions-enabled>
120
+ <system-properties>
121
+ <property name="jruby.management.enabled" value="false" />
122
+ <property name="os.arch" value="" />
123
+ <property name="jruby.compile.mode" value="JIT"/> <!-- JIT|FORCE|OFF -->
124
+ <property name="jruby.compile.fastest" value="true"/>
125
+ <property name="jruby.compile.frameless" value="true"/>
126
+ <property name="jruby.compile.positionless" value="true"/>
127
+ <property name="jruby.compile.threadless" value="false"/>
128
+ <property name="jruby.compile.fastops" value="false"/>
129
+ <property name="jruby.compile.fastcase" value="false"/>
130
+ <property name="jruby.compile.chainsize" value="500"/>
131
+ <property name="jruby.compile.lazyHandles" value="false"/>
132
+ <property name="jruby.compile.peephole" value="true"/>
133
+ </system-properties>
134
+ </appengine-web-app>
135
+ APPENGINE
136
+ end
137
+
138
+ def warble_config_contents
139
+ <<-WARBLE
140
+ Warbler::Config.new do |config|
141
+ config.gems = %w( rave json-jruby rack builder )
142
+ config.includes = %w( robot.rb appengine-web.xml )
143
+ end
144
+ WARBLE
145
+ end
@@ -0,0 +1,8 @@
1
+ #Starts up rack based on the config.ru file in the working directory
2
+ # Note that this is of limited use right now, because robots have to
3
+ # run on appengine. Better to test locally with the appengine sdk
4
+ def start_robot(args)
5
+ cmd = (RUBY_PLATFORM == 'java') ? "jruby -S rackup" : "rackup"
6
+ cmd += " " + args.join(" ") if args
7
+ exec(cmd)
8
+ end
@@ -0,0 +1,13 @@
1
+ #Display usage for rave command
2
+ def display_usage
3
+ puts "Useage: rave [create | server | war] [robot_name] [options]"
4
+ puts "'create' generates a Google Wave robot client stub application."
5
+ puts "e.g."
6
+ puts "rave create my_robot image_url=http://my_robot.appspot.com/image.png profile_url=http://my_robot.appspot.com/"
7
+ puts "'server' launches the robot"
8
+ puts "e.g."
9
+ puts "rave server"
10
+ puts "'war' creates a war file suitable for deploying to Google AppEngine"
11
+ puts "e.g."
12
+ puts "rave war"
13
+ end
@@ -0,0 +1,51 @@
1
+ #Runs warbler to package up the robot
2
+ # then does some cleanup that is specific to App Engine:
3
+ # => Deletes the complete JRuby jar from both the app's lib folder and
4
+ # the frozen warbler gem, and replaces them with a broken version
5
+ # => Changes the file path json-jruby
6
+ # TODO: Not sure why this is necessary, but it doesn't run on appengine without it
7
+ def create_war(args)
8
+ #Run warbler
9
+ system("jruby -S warble")
10
+ web_inf = File.join(".", "tmp", "war", "WEB-INF")
11
+ rave_jars = File.join(File.dirname(__FILE__), "..", "jars")
12
+ #Delete the complete JRuby jar that warbler sticks in lib
13
+ delete_jruby_from_lib(File.join(web_inf, "lib"))
14
+ #Delete the complete JRuby jar from warbler itself
15
+ delete_jruby_from_warbler(File.join(web_inf, "gems", "gems"))
16
+ #Copy the broken up JRuby jar into warbler #TODO Is warbler necessary? Can we just delete warbler?
17
+ copy_jruby_chunks_to_warbler(rave_jars, Dir[File.join(web_inf, "gems", "gems", "warbler-*", "lib")].first)
18
+ #Fix the broken paths in json-jruby
19
+ fix_json_jruby_paths(File.join(web_inf, "gems", "gems"))
20
+ end
21
+
22
+ def delete_jruby_from_lib(web_inf_lib)
23
+ jar = Dir[File.join(web_inf_lib, "jruby-complete-*.jar")].first
24
+ puts "Deleting #{jar}"
25
+ File.delete(jar) if jar
26
+ end
27
+
28
+ def delete_jruby_from_warbler(web_inf_gems)
29
+ jar = Dir[File.join(web_inf_gems, "warbler-*", "lib", "jruby-complete-*.jar")].first
30
+ puts "Deleting #{jar}"
31
+ File.delete(jar) if jar
32
+ end
33
+
34
+ def copy_jruby_chunks_to_warbler(rave_jar_dir, warbler_jar_dir)
35
+ puts "Copying jruby chunks"
36
+ %w( jruby-core.jar ruby-stdlib.jar ).each do |jar|
37
+ File.copy(File.join(rave_jar_dir, jar), File.join(warbler_jar_dir, jar))
38
+ end
39
+ end
40
+
41
+ def fix_json_jruby_paths(web_inf_gems)
42
+ #TODO: Why is this necessary? Is this an appengine issue?
43
+ puts "Fixing paths in json-jruby"
44
+ ext = Dir[File.join(web_inf_gems, "json-jruby-*", "lib", "json", "ext.rb")].first
45
+ if ext
46
+ text = File.open(ext, "r") { |f| f.read }
47
+ text.gsub!("require 'json/ext/parser'", "require 'ext/parser'")
48
+ text.gsub!("require 'json/ext/generator'", "require 'ext/generator'")
49
+ File.open(ext, "w") { |f| f.write(text) }
50
+ end
51
+ end
data/lib/exceptions.rb ADDED
@@ -0,0 +1,6 @@
1
+ module Rave
2
+ #Exception raised when registering an invalid event
3
+ class InvalidEventException < Exception ; end
4
+ #Exception raised when registering an event with an invalid handler
5
+ class InvalidHandlerException < Exception ; end
6
+ end
Binary file
Binary file
@@ -0,0 +1,40 @@
1
+ #Contains the Rack #call method - to be mixed in to the Robot class
2
+ module Rave
3
+ module Mixins
4
+ module Controller
5
+
6
+ LOGGER = java.util.logging.Logger.getLogger("Controller")
7
+
8
+ def call(env)
9
+ request = Rack::Request.new(env)
10
+ path = request.path_info
11
+ method = request.request_method
12
+ LOGGER.info("#{method}ing #{path}")
13
+ begin
14
+ #There are only 3 URLs that Wave can access:
15
+ # robot capabilities, robot profile, and event notification
16
+ if path == "/_wave/capabilities.xml" && method == "GET"
17
+ [ 200, { 'Content-Type' => 'text/xml' }, capabilities_xml ]
18
+ elsif path == "/_wave/robot/profile" && method == "GET"
19
+ [ 200, { 'Content-Type' => 'application/json' }, profile_json ]
20
+ elsif path == "/_wave/robot/jsonrpc" && method == "POST"
21
+ body = request.body.read
22
+ context, events = parse_json_body(body)
23
+ events.each do |event|
24
+ handle_event(event, context)
25
+ end
26
+ [ 200, { 'Content-Type' => 'application/json' }, context.to_json ]
27
+ else
28
+ #TODO: Also, give one more option: respond_to?(:non_robot_url) or something - can override in impl
29
+ #TODO: Log this
30
+ [ 404, { 'Content-Type' => 'text/html' }, "404 - Not Found" ]
31
+ end
32
+ rescue Exception => e
33
+ #TODO: Log this
34
+ [ 500, { 'Content-Type' => 'text/html' }, "500 - Internal Server Error"]
35
+ end
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,168 @@
1
+ #This mixin provides methods for robots to deal with parsing and presenting JSON and XML
2
+ module Rave
3
+ module Mixins
4
+ module DataFormat
5
+
6
+ LOGGER = java.util.logging.Logger.getLogger("DataFormat") unless defined?(LOGGER)
7
+
8
+ #Returns this robot's capabilities in XML
9
+ def capabilities_xml
10
+ xml = Builder::XmlMarkup.new
11
+ xml.instruct!
12
+ xml.tag!("w:robot", "xmlns:w" => "http://wave.google.com/extensions/robots/1.0") do
13
+ xml.tag!("w:capabilities") do
14
+ @handlers.keys.each do |capability|
15
+ xml.tag!("w:capability", "name" => capability)
16
+ end
17
+ end
18
+ unless @cron_jobs.empty?
19
+ xml.tag!("w:crons") do
20
+ @cron_jobs.each do |job|
21
+ xml.tag!("w:cron", "path" => job[:path], "timeinseconds" => job[:seconds])
22
+ end
23
+ end
24
+ end
25
+ attrs = { "name" => @name }
26
+ attrs["imageurl"] = @image_url if @image_url
27
+ attrs["profileurl"] = @profile_url if @profile_url
28
+ xml.tag!("w:profile", attrs)
29
+ end
30
+ end
31
+
32
+ #Returns the robot's profile in json format
33
+ def profile_json
34
+ {
35
+ "name" => @name,
36
+ "imageurl" => @image_url,
37
+ "profile_url" => @profile_url
38
+ }.to_json
39
+ end
40
+
41
+ #Parses context and event info from JSON input
42
+ def parse_json_body(json)
43
+ LOGGER.info("Parsing JSON:")
44
+ LOGGER.info(json.to_s)
45
+ data = JSON.parse(json)
46
+ #Create Context
47
+ context = context_from_json(data)
48
+ #Create events
49
+ events = events_from_json(data)
50
+ return context, events
51
+ end
52
+
53
+ protected
54
+ def context_from_json(json)
55
+ blips = {}
56
+ blips_from_json(json).each do |blip|
57
+ blips[blip.id] = blip
58
+ end
59
+ wavelets = {}
60
+ wavelets_from_json(json).each do |wavelet|
61
+ wavelets[wavelet.id] = wavelet
62
+ end
63
+ waves = {}
64
+ #Waves aren't sent back, but we can reconstruct them from the wavelets
65
+ waves_from_wavelets(wavelets).each do |wave|
66
+ waves[wave.id] = wave
67
+ end
68
+ Rave::Models::Context.new(
69
+ :waves => waves,
70
+ :wavelets => wavelets,
71
+ :blips => blips
72
+ )
73
+ end
74
+
75
+ def blips_from_json(json)
76
+ if json['blips']
77
+ json['blips']['map'].values.collect do |blip_data|
78
+ blip = Rave::Models::Blip.new(
79
+ :id => blip_data['blipId'],
80
+ :annotations => annotations_from_json(json),
81
+ :child_blip_ids => blip_data['childBlipIds'],
82
+ :content => blip_data['content'],
83
+ :contributors => blip_data['contributors'],
84
+ :creator => blip_data['creator'],
85
+ :elements => blip_data['elements'],
86
+ :last_modified_time => blip_data['lastModifiedTime'],
87
+ :parent_blip_id => blip_data['parentBlipId'],
88
+ :version => blip_data['version'],
89
+ :wave_id => blip_data['waveId'],
90
+ :wavelet_id => blip_data['waveletId']
91
+ )
92
+ end
93
+ else
94
+ []
95
+ end
96
+ end
97
+
98
+ def annotations_from_json(json)
99
+ if json['annotation']
100
+ json['annotations'].collect do |annotation|
101
+ Rave::Models::Annotation.new(
102
+ :name => annotation['name'],
103
+ :value => annotation['value'],
104
+ :range => Range.new(annotation['range']['start'], annotation['range']['end'])
105
+ )
106
+ end
107
+ else
108
+ []
109
+ end
110
+ end
111
+
112
+ def events_from_json(json)
113
+ if json['events'] && json['events']['list']
114
+ json['events']['list'].collect do |event|
115
+ properties = {}
116
+ event['properties']['map'].each { |key, value| properties[key] = value['list'] }
117
+ Rave::Models::Event.new(
118
+ :type => event['type'],
119
+ :timestamp => event['timestamp'],
120
+ :modified_by => event['modifiedBy'],
121
+ :properties => properties
122
+ )
123
+ end
124
+ else
125
+ []
126
+ end
127
+ end
128
+
129
+ def wavelets_from_json(json)
130
+ #Currently only one wavelet is sent back
131
+ #TODO: should this look at the wavelet's children too?
132
+ wavelet = json['wavelet']
133
+ if wavelet
134
+ [
135
+ Rave::Models::Wavelet.new(
136
+ :creator => wavelet['creator'],
137
+ :creation_time => wavelet['creationTime'],
138
+ :data_documents => wavelet['dataDocuments'],
139
+ :last_modifed_time => wavelet['lastModifiedTime'],
140
+ :participants => wavelet['participants'],
141
+ :root_blip_id => wavelet['rootBlipId'],
142
+ :title => wavelet['title'],
143
+ :version => wavelet['version'],
144
+ :wave_id => wavelet['waveId'],
145
+ :id => wavelet['waveletId']
146
+ )
147
+ ]
148
+ else
149
+ []
150
+ end
151
+ end
152
+
153
+ def waves_from_wavelets(wavelets)
154
+ wave_wavelet_map = {}
155
+ if wavelets
156
+ wavelets.values.each do |wavelet|
157
+ wave_wavelet_map[wavelet.wave_id] ||= []
158
+ wave_wavelet_map[wavelet.wave_id] << wavelet.id
159
+ end
160
+ end
161
+ wave_wavelet_map.collect do |wave_id, wavelet_ids|
162
+ Rave::Models::Wave.new(:id => wave_id, :wavelet_ids => wavelet_ids)
163
+ end
164
+ end
165
+
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,18 @@
1
+ module Rave
2
+ module Models
3
+ class Annotation
4
+ attr_reader :name, :value, :range
5
+
6
+ #Options include
7
+ # - :name
8
+ # - :value
9
+ # - :range
10
+ def initialize(options = {})
11
+ @name = options[:name]
12
+ @value = options[:value]
13
+ @range = options[:range]
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,61 @@
1
+ #Represents a Blip, owned by a Wavelet
2
+ module Rave
3
+ module Models
4
+ class Blip
5
+ attr_reader :id, :annotations, :child_blip_ids, :content, :constributors, :creator,
6
+ :elements, :last_modified_time, :parent_blip_id, :version, :wave_id, :wavelet_id
7
+ attr_accessor :context
8
+
9
+ #Options include:
10
+ # - :annotations
11
+ # - :child_blip_ids
12
+ # - :content
13
+ # - :contributors
14
+ # - :creator
15
+ # - :elements
16
+ # - :last_modified_time
17
+ # - :parent_blip_id
18
+ # - :version
19
+ # - :wave_id
20
+ # - :wavelet_id
21
+ # - :id
22
+ # - :context
23
+ def initialize(options = {})
24
+ @annotations = options[:annotations] || []
25
+ @child_blip_ids = Set.new(options[:child_blip_ids])
26
+ @content = options[:content]
27
+ @contributors = Set.new(options[:contributors])
28
+ @creator = options[:creator]
29
+ @elements = options[:elements] || {}
30
+ @last_modified_time = options[:last_modified_time] || Time.now
31
+ @parent_blip_id = options[:parent_blip_id]
32
+ @version = options[:version] || -1
33
+ @wave_id = options[:wave_id]
34
+ @wavelet_id = options[:wavelet_id]
35
+ @id = options[:id]
36
+ @context = options[:context]
37
+ end
38
+
39
+ #Returns true if this is a root blip (no parent blip)
40
+ def root?
41
+ @parent_blip_id.nil?
42
+ end
43
+
44
+ #Returns true if an annotation with the given name exists in this blip
45
+ def has_annotation?(name)
46
+ @annotations.any? { |a| a.name == name }
47
+ end
48
+
49
+ #Creates a child blip under this blip
50
+ def create_child_blip
51
+ #TODO
52
+ end
53
+
54
+ #Delete this blip from its wavelet
55
+ def delete
56
+ #TODO
57
+ end
58
+
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,45 @@
1
+ #Contains server request information including current waves and operations
2
+ module Rave
3
+ module Models
4
+ class Context
5
+ attr_reader :waves, :wavelets, :blips, :operations
6
+
7
+ #Options include:
8
+ # - :waves
9
+ # - :wavelets
10
+ # - :blips
11
+ # - :operations
12
+ def initialize(options = {})
13
+ @waves = options[:waves] || {}
14
+ @waves.values.each { |wave| wave.context = self } #Set up self as this wave's context
15
+ @wavelets = options[:wavelets] || {}
16
+ @wavelets.values.each { |wavelet| wavelet.context = self } #Set up self as this wavelet's context
17
+ @blips = options[:blips] || {}
18
+ @blips.values.each { |blip| blip.context = self } #Set up self as this blip's context
19
+ @operations = options[:operations] || []
20
+ end
21
+
22
+ #Find the root wavelet if it exists in this context
23
+ def root_wavelet
24
+ @wavelets.values.find { |wavelet| wavelet.id =~ Regexp.new(Rave::Models::Wavelet::ROOT_ID_REGEXP) }
25
+ end
26
+
27
+ #Serializes the context to JSON format
28
+ def to_json
29
+ self.to_hash.to_json
30
+ end
31
+
32
+ #Serialize the context to a hash map
33
+ def to_hash
34
+ hash = {
35
+ 'operations' => { 'javaClass' => 'java.util.ArrayList', 'list' => [] },
36
+ 'javaClass' => 'com.google.wave.api.impl.OperationMessageBundle'
37
+ }
38
+ @operations.each do |op|
39
+ hash['operations']['list'] << op.to_hash
40
+ end
41
+ hash
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,9 @@
1
+ module Rave
2
+ module Models
3
+ class Document
4
+
5
+ #TODO
6
+
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,48 @@
1
+ #Represents and event
2
+ module Rave
3
+ module Models
4
+ class Event
5
+ attr_reader :type, :timestamp, :modified_by, :properties
6
+
7
+ #Event types:
8
+ WAVELET_BLIP_CREATED = 'WAVELET_BLIP_CREATED'
9
+ WAVELET_BLIP_REMOVED = 'WAVELET_BLIP_REMOVED'
10
+ WAVELET_PARTICIPANTS_CHANGED = 'WAVELET_PARTICIPANTS_CHANGED'
11
+ WAVELET_TIMESTAMP_CHANGED = 'WAVELET_TIMESTAMP_CHANGED'
12
+ WAVELET_TITLE_CHANGED = 'WAVELET_TITLE_CHANGED'
13
+ WAVELET_VERSION_CHANGED = 'WAVELET_VERSION_CHANGED'
14
+ BLIP_CONTRIBUTORS_CHANGED = 'BLIP_CONTRIBUTORS_CHANGED'
15
+ BLIP_DELETED = 'BLIP_DELETED'
16
+ BLIP_SUBMITTED = 'BLIP_SUBMITTED'
17
+ BLIP_TIMESTAMP_CHANGED = 'BLIP_TIMESTAMP_CHANGED'
18
+ BLIP_VERSION_CHANGED = 'BLIP_VERSION_CHANGED'
19
+ DOCUMENT_CHANGED = 'DOCUMENT_CHANGED'
20
+ FORM_BUTTON_CLICKED = 'FORM_BUTTON_CLICKED'
21
+
22
+ VALID_EVENTS = [
23
+ WAVELET_BLIP_CREATED, WAVELET_BLIP_REMOVED, WAVELET_PARTICIPANTS_CHANGED,
24
+ WAVELET_TIMESTAMP_CHANGED, WAVELET_TITLE_CHANGED, WAVELET_VERSION_CHANGED,
25
+ BLIP_CONTRIBUTORS_CHANGED, BLIP_DELETED, BLIP_SUBMITTED, BLIP_TIMESTAMP_CHANGED,
26
+ BLIP_VERSION_CHANGED, DOCUMENT_CHANGED, FORM_BUTTON_CLICKED
27
+ ]
28
+
29
+ #Options include:
30
+ # - :type
31
+ # - :timestamp
32
+ # - :modified_by
33
+ # - :properties
34
+ def initialize(options = {})
35
+ @type = options[:type]
36
+ @timestamp = options[:timestamp] || Time.now
37
+ @modified_by = options[:modified_by]
38
+ @properties = options[:properties] || {}
39
+ end
40
+
41
+ #Returns true if the event_type is a possible event type, and false if not
42
+ def self.valid_event_type?(event_type)
43
+ VALID_EVENTS.include?(event_type)
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,89 @@
1
+ #Represents an operation to be applied on the server
2
+ module Rave
3
+ module Models
4
+ class Operation
5
+ attr_reader :type, :wave_id, :wavelet_id, :blip_id, :index, :property
6
+
7
+ #Constants
8
+ # Types of operations
9
+ WAVELET_APPEND_BLIP = 'WAVELET_APPEND_BLIP'
10
+ WAVELET_ADD_PARTICIPANT = 'WAVELET_ADD_PARTICIPANT'
11
+ WAVELET_CREATE = 'WAVELET_CREATE'
12
+ WAVELET_REMOVE_SELF = 'WAVELET_REMOVE_SELF'
13
+ WAVELET_DATADOC_SET = 'WAVELET_DATADOC_SET'
14
+ WAVELET_SET_TITLE = 'WAVELET_SET_TITLE'
15
+ BLIP_CREATE_CHILD = 'BLIP_CREATE_CHILD'
16
+ BLIP_DELETE = 'BLIP_DELETE'
17
+ DOCUMENT_ANNOTATION_DELETE = 'DOCUMENT_ANNOTATION_DELETE'
18
+ DOCUMENT_ANNOTATION_SET = 'DOCUMENT_ANNOTATION_SET'
19
+ DOCUMENT_ANNOTATION_SET_NORANGE = 'DOCUMENT_ANNOTATION_SET_NORANGE'
20
+ DOCUMENT_APPEND = 'DOCUMENT_APPEND'
21
+ DOCUMENT_APPEND_STYLED_TEXT = 'DOCUMENT_APPEND_STYLED_TEXT'
22
+ DOCUMENT_INSERT = 'DOCUMENT_INSERT'
23
+ DOCUMENT_DELETE = 'DOCUMENT_DELETE'
24
+ DOCUMENT_REPLACE = 'DOCUMENT_REPLACE'
25
+ DOCUMENT_ELEMENT_APPEND = 'DOCUMENT_ELEMENT_APPEND'
26
+ DOCUMENT_ELEMENT_DELETE = 'DOCUMENT_ELEMENT_DELETE'
27
+ DOCUMENT_ELEMENT_INSERT = 'DOCUMENT_ELEMENT_INSERT'
28
+ DOCUMENT_ELEMENT_INSERT_AFTER = 'DOCUMENT_ELEMENT_INSERT_AFTER'
29
+ DOCUMENT_ELEMENT_INSERT_BEFORE = 'DOCUMENT_ELEMENT_INSERT_BEFORE'
30
+ DOCUMENT_ELEMENT_REPLACE = 'DOCUMENT_ELEMENT_REPLACE'
31
+ DOCUMENT_INLINE_BLIP_APPEND = 'DOCUMENT_INLINE_BLIP_APPEND'
32
+ DOCUMENT_INLINE_BLIP_DELETE = 'DOCUMENT_INLINE_BLIP_DELETE'
33
+ DOCUMENT_INLINE_BLIP_INSERT = 'DOCUMENT_INLINE_BLIP_INSERT'
34
+ DOCUMENT_INLINE_BLIP_INSERT_AFTER_ELEMENT = 'DOCUMENT_INLINE_BLIP_INSERT_AFTER_ELEMENT'
35
+
36
+ #Options include:
37
+ # - :type
38
+ # - :wave_id
39
+ # - :wavelet_id
40
+ # - :blip_id
41
+ # - :index
42
+ # - :property
43
+ def initialize(options = {})
44
+ @type = options[:type]
45
+ @wave_id = options[:wave_id]
46
+ @wavelet_id = options[:wavelet_id]
47
+ @blip_id = options[:blip_id]
48
+ @index = options[:index] || -1
49
+ @property = options[:property]
50
+ end
51
+
52
+ #Serialize the operation to hash
53
+ def to_hash
54
+ {
55
+ 'blipId' => @blip_id,
56
+ 'index' => @index,
57
+ 'waveletId' => @wavelet_id,
58
+ 'waveId' => @wave_id,
59
+ 'type' => @type,
60
+ 'javaClass' => 'com.google.wave.api.impl.OperationImpl',
61
+ 'property' => property_to_hash
62
+ }
63
+ end
64
+
65
+ #Serialize teh operation to json
66
+ def to_json
67
+ to_hash.to_json
68
+ end
69
+
70
+ protected
71
+ #Decide what kind of property it is and return the value that should be in the JSON
72
+ def property_to_hash
73
+ if @property.kind_of?(String)
74
+ @property
75
+ elsif @property.kind_of?(Range)
76
+ {
77
+ 'javaClass' => 'com.google.wave.api.Range',
78
+ 'start' => @property.first,
79
+ 'end' => @property.last
80
+ }
81
+ else
82
+ #TODO: What else needs to be in here?
83
+ raise "I don't know what that property is..."
84
+ end
85
+ end
86
+
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,61 @@
1
+ #Contains Robot data, event handlers and cron jobs
2
+ module Rave
3
+ module Models
4
+ class Robot
5
+ include Rave::Mixins::DataFormat
6
+ include Rave::Mixins::Controller
7
+
8
+ attr_reader :name, :image_url, :profile_url
9
+
10
+ #Options include:
11
+ # - :name
12
+ # - :image_url
13
+ # - :profile_url
14
+ def initialize(options = {})
15
+ @name = options[:name]
16
+ @image_url = options[:image_url]
17
+ @profile_url = options[:profile_url]
18
+ @handlers = {}
19
+ @cron_jobs = []
20
+ register_default_handlers
21
+ end
22
+
23
+ #Register a handler
24
+ # event_type is a string, and must be one of Rave::Models::Event::VALID_EVENTS
25
+ # multiple handlers may be applied to an event
26
+ def register_handler(event_type, handler)
27
+ raise Rave::InvalidEventException.new("Unknown event: #{event_type}") unless Rave::Models::Event.valid_event_type?(event_type)
28
+ raise Rave::InvalidHandlerException.new("Unknown handler: #{handler}") unless self.respond_to?(handler)
29
+ @handlers[event_type] ||= []
30
+ @handlers[event_type] << handler unless @handlers[event_type].include?(handler)
31
+ end
32
+
33
+ #Dispatches events to the appropriate handler
34
+ def handle_event(event, context)
35
+ #Ignore unhandled events
36
+ if (handlers = @handlers[event.type])
37
+ handlers.each do |handler|
38
+ self.send(handler, event, context)
39
+ end
40
+ end
41
+ end
42
+
43
+ #Registers a cron job
44
+ def register_cron_job(path, seconds)
45
+ @cron_jobs << {:path => path, :seconds => seconds}
46
+ end
47
+
48
+ protected
49
+ #Register any handlers that are defined through naming convention
50
+ def register_default_handlers
51
+ Event::VALID_EVENTS.each do |event|
52
+ listener = event.downcase.to_sym
53
+ if respond_to?(listener)
54
+ register_handler(event, listener)
55
+ end
56
+ end
57
+ end
58
+
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,19 @@
1
+ # Represents a Wave
2
+ module Rave
3
+ module Models
4
+ class Wave
5
+ attr_reader :id, :wavelet_ids
6
+
7
+ attr_accessor :context
8
+
9
+ #Options include:
10
+ # - :wavelet_ids
11
+ # - :id
12
+ def initialize(options = {})
13
+ @id = options[:id]
14
+ @wavelet_ids = Set.new(options[:wavelet_ids])
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,69 @@
1
+ # Represents a Wavelet, owned by a Wave
2
+ module Rave
3
+ module Models
4
+ class Wavelet
5
+ attr_reader :creator, :creation_time, :data_documents, :last_modified_time,
6
+ :participants, :root_blip_id, :title, :version, :wave_id, :id
7
+ attr_accessor :context #Context needs to be able to set this
8
+
9
+ ROOT_ID_SUFFIX = "conv+root" #The suffix for the root wavelet in a wave]
10
+ ROOT_ID_REGEXP = /conv\+root$/
11
+
12
+ # Options include:
13
+ # - :creator
14
+ # - :creation_time
15
+ # - :data_documents
16
+ # - :last_modifed_time
17
+ # - :participants
18
+ # - :root_blip_id
19
+ # - :title
20
+ # - :version
21
+ # - :wave_id
22
+ # - :context
23
+ # - :id
24
+ def initialize(options = {})
25
+ @creator = options[:creator]
26
+ @creation_time = options[:creation_time] || Time.now
27
+ @data_documents = options[:data_documents] || {}
28
+ @last_modified_time = options[:last_modified_time] || Time.now
29
+ @participants = Set.new(options[:participants])
30
+ @root_blip_id = options[:root_blip_id]
31
+ @title = options[:title]
32
+ @version = options[:version] || 0
33
+ @wave_id = options[:wave_id]
34
+ @context = options[:context]
35
+ @id = options[:id]
36
+ end
37
+
38
+ #Creates a blip for this wavelet
39
+ def create_blip
40
+ #TODO
41
+ blip = Blip.new(:wave_id => @wave_id, :wavelet_id => @id)
42
+ @context.operations << Operation.new(:type => Operation::WAVELET_APPEND_BLIP, :wave_id => @wave_id, :wavelet_id => @id, :prop => blip)
43
+ @context.add_blip(blip)
44
+ blip
45
+ end
46
+
47
+ #Adds a participant to the wavelet
48
+ def add_participant(participant_id)
49
+ #TODO
50
+ end
51
+
52
+ #Removes this robot from the wavelet
53
+ def remove_robot
54
+ #TODO
55
+ end
56
+
57
+ #Sets the data document for the wavelet
58
+ def set_data_document(name, data)
59
+ #TODO
60
+ end
61
+
62
+ #Set the title
63
+ def title=(title)
64
+ @title = title
65
+ end
66
+
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,131 @@
1
+ #Reopen the blip class and add operation-related methods
2
+ module Rave
3
+ module Models
4
+ class Blip
5
+
6
+ #Clear the content
7
+ def clear
8
+ @context.operations << Operation.new(
9
+ :type => Operation::DOCUMENT_DELETE,
10
+ :blip_id => @id,
11
+ :wavelet_id => @wavelet_id,
12
+ :wave_id => @wave_id,
13
+ :index => 0,
14
+ :property => 0..(@content ? @content.length : 0)
15
+ )
16
+ @content = ''
17
+ end
18
+
19
+ #Insert text at an index
20
+ def insert_text(text, index)
21
+ @context.operations << Operation.new(
22
+ :type => Operation::DOCUMENT_INSERT,
23
+ :blip_id => @id,
24
+ :wavelet_id => @wavelet_id,
25
+ :wave_id => @wave_id,
26
+ :index => index,
27
+ :property => text
28
+ )
29
+ @content = @content ? @content[0, index] + text + @content[index, @content.length - index] : text
30
+ end
31
+
32
+ #Set the content text of the blip
33
+ def set_text(text)
34
+ clear
35
+ insert_text(text, 0)
36
+ end
37
+
38
+ #Deletes the text in a given range and replaces it with the given text
39
+ def set_text_in_range(range, text)
40
+ delete_range(range)
41
+ insert_text(text, range.first)
42
+ end
43
+
44
+ #Appends text to the end of the content
45
+ def append_text(text)
46
+ @context.operations << Operation.new(
47
+ :type => Operation::DOCUMENT_APPEND,
48
+ :blip_id => @id,
49
+ :wavelet_id => @wavelet_id,
50
+ :wave_id => @wave_id,
51
+ :property => text
52
+ )
53
+ @content = @content + text
54
+ end
55
+
56
+ #Deletes text in the given range
57
+ def delete_range(range)
58
+ @context.operations << Operation.new(
59
+ :type => Operation::DOCUMENT_DELETE,
60
+ :blip_id => @id,
61
+ :wavelet_id => @wavelet_id,
62
+ :wave_id => @wave_id,
63
+ :index => range.first,
64
+ :property => range
65
+ )
66
+ @content = @content[0..range.first-1] + @content[range.last+1..@content.length-1]
67
+ end
68
+
69
+ #Annotates the entire content
70
+ def annotate_document(name, value)
71
+ #TODO
72
+ raise "This hasn't been implemented yet"
73
+ end
74
+
75
+ #Deletes the annotation with the given name
76
+ def delete_annotation_by_name(name)
77
+ #TODO
78
+ raise "This hasn't been implemented yet"
79
+ end
80
+
81
+ #Deletes the annotations with the given key in the given range
82
+ def delete_annotation_in_range(range, name)
83
+ #TODO
84
+ raise "This hasn't been implemented yet"
85
+ end
86
+
87
+ #Appends an inline blip to this blip
88
+ def append_inline_blip
89
+ #TODO
90
+ raise "This hasn't been implemented yet"
91
+ end
92
+
93
+ #Deletes an inline blip from this blip
94
+ def delete_inline_blip(blip_id)
95
+ #TODO
96
+ raise "This hasn't been implemented yet"
97
+ end
98
+
99
+ #Inserts an inline blip at the given position
100
+ def insert_inline_blip(position)
101
+ #TODO
102
+ raise "This hasn't been implemented yet"
103
+ end
104
+
105
+ #Deletes an element at the given position
106
+ def delete_element(position)
107
+ #TODO
108
+ raise "This hasn't been implemented yet"
109
+ end
110
+
111
+ #Inserts the given element in the given position
112
+ def insert_element(position, element)
113
+ #TODO
114
+ raise "This hasn't been implemented yet"
115
+ end
116
+
117
+ #Replaces the element at the given position with the given element
118
+ def replace_element(position, element)
119
+ #TODO
120
+ raise "This hasn't been implemented yet"
121
+ end
122
+
123
+ #Appends an element
124
+ def append_element(element)
125
+ #TODO
126
+ raise "This hasn't been implemented yet"
127
+ end
128
+
129
+ end
130
+ end
131
+ end
data/lib/rave.rb ADDED
@@ -0,0 +1,23 @@
1
+ require 'rubygems'
2
+ require 'set'
3
+ require 'builder'
4
+ require 'json'
5
+
6
+ here = File.dirname(__FILE__)
7
+ mixins = File.join(here, "mixins")
8
+ models = File.join(here, "models")
9
+ ops = File.join(here, "ops")
10
+
11
+ require File.join(here, 'exceptions')
12
+
13
+ %w( data_format controller ).each do |dep|
14
+ require File.join(mixins, dep)
15
+ end
16
+
17
+ %w( annotation blip context document event operation robot wave wavelet).each do |dep|
18
+ require File.join(models, dep)
19
+ end
20
+
21
+ %w( blip_ops ).each do |dep|
22
+ require File.join(ops, dep)
23
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ extensions: []
3
+
4
+ homepage: http://github.com/diminish7/rave
5
+ executables:
6
+ - rave
7
+ version: !ruby/object:Gem::Version
8
+ version: 0.1.0
9
+ post_install_message:
10
+ date: 2009-06-20 07:00:00 +00:00
11
+ files:
12
+ - lib/exceptions.rb
13
+ - lib/rave.rb
14
+ - lib/commands/create.rb
15
+ - lib/commands/server.rb
16
+ - lib/commands/usage.rb
17
+ - lib/commands/war.rb
18
+ - lib/jars/appengine-api-1.0-sdk-1.2.1.jar
19
+ - lib/jars/jruby-core.jar
20
+ - lib/jars/ruby-stdlib.jar
21
+ - lib/mixins/controller.rb
22
+ - lib/mixins/data_format.rb
23
+ - lib/models/annotation.rb
24
+ - lib/models/blip.rb
25
+ - lib/models/context.rb
26
+ - lib/models/document.rb
27
+ - lib/models/event.rb
28
+ - lib/models/operation.rb
29
+ - lib/models/robot.rb
30
+ - lib/models/wave.rb
31
+ - lib/models/wavelet.rb
32
+ - lib/ops/blip_ops.rb
33
+ - bin/rave
34
+ rubygems_version: 1.3.3
35
+ rdoc_options: []
36
+
37
+ signing_key:
38
+ cert_chain: []
39
+
40
+ name: rave
41
+ has_rdoc: true
42
+ platform: ruby
43
+ summary: A Google Wave robot client API for Ruby
44
+ default_executable:
45
+ bindir: bin
46
+ licenses: []
47
+
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ version:
50
+ requirements:
51
+ - - '>='
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ version:
56
+ requirements:
57
+ - - '>='
58
+ - !ruby/object:Gem::Version
59
+ version: 1.8.6
60
+ require_paths:
61
+ - lib
62
+ specification_version: 3
63
+ test_files: []
64
+
65
+ dependencies:
66
+ - !ruby/object:Gem::Dependency
67
+ type: :runtime
68
+ name: rack
69
+ version_requirement:
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ version:
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: "1.0"
76
+ - !ruby/object:Gem::Dependency
77
+ type: :runtime
78
+ name: builder
79
+ version_requirement:
80
+ version_requirements: !ruby/object:Gem::Requirement
81
+ version:
82
+ requirements:
83
+ - - '>='
84
+ - !ruby/object:Gem::Version
85
+ version: 2.1.2
86
+ - !ruby/object:Gem::Dependency
87
+ type: :runtime
88
+ name: json-jruby
89
+ version_requirement:
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ version:
92
+ requirements:
93
+ - - '>='
94
+ - !ruby/object:Gem::Version
95
+ version: 1.1.6
96
+ - !ruby/object:Gem::Dependency
97
+ type: :runtime
98
+ name: warbler
99
+ version_requirement:
100
+ version_requirements: !ruby/object:Gem::Requirement
101
+ version:
102
+ requirements:
103
+ - - '>='
104
+ - !ruby/object:Gem::Version
105
+ version: 0.9.13
106
+ description: A toolkit for building Google Wave robots in Ruby
107
+ email: diminish7@gmail.com
108
+ authors:
109
+ - Jason Rush
110
+ - Jay Donnell
111
+ extra_rdoc_files: []
112
+
113
+ requirements: []
114
+
115
+ rubyforge_project: rave
116
+ autorequire: