rave 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: