rave 0.1.1 → 0.1.2

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.
@@ -1,13 +1,19 @@
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"
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
+ puts "'appengine_deploy' deploys the tmp/war folder to Google AppEngine"
14
+ puts "e.g."
15
+ puts "rave appengine_deploy"
16
+ puts "'cleanup' removes the .war file and the tmp/war staging directory"
17
+ puts "e.g."
18
+ puts "rave cleanup"
13
19
  end
data/lib/commands/war.rb CHANGED
@@ -1,51 +1,28 @@
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
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require File.join(File.dirname(__FILE__), 'task.rb')
4
+
5
+ #Runs warbler to package up the robot
6
+ # then does some cleanup that is specific to App Engine:
7
+ # => Deletes the complete JRuby jar from both the app's lib folder and
8
+ # the frozen warbler gem, and replaces them with a broken up version
9
+ # => Changes the file path json-jruby
10
+ # TODO: Not sure why this is necessary, but it doesn't run on appengine without it
11
+ def create_war(args)
12
+ Rake.application.standard_exception_handling do
13
+ Rake.application.init
14
+ Rave::Task.new
15
+ task(:default => "rave:create_war")
16
+ Rake.application.top_level
17
+ end
18
+ end
19
+
20
+ #Runs warbler's cleanup to get rid of the .war file and the tmp/war folder
21
+ def cleanup_war(args)
22
+ Rake.application.standard_exception_handling do
23
+ Rake.application.init
24
+ Rave::Task.new
25
+ task(:default => "rave:clean")
26
+ Rake.application.top_level
27
+ end
51
28
  end
data/lib/exceptions.rb CHANGED
@@ -1,6 +1,20 @@
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
1
+ module Rave
2
+ #Exception raised when registering an invalid event
3
+ class InvalidEventException < Exception ; end
4
+
5
+ #Exception raised when registering an event with an invalid handler
6
+ class InvalidHandlerException < Exception ; end
7
+
8
+ # Raised when trying to create an object with the same ID as one that already exists.
9
+ class DuplicatedIDError < Exception; end
10
+
11
+ # Raised if an unimplemented method is called.
12
+ class NotImplementedError < Exception; end
13
+
14
+ # A method option was not one of the values allowed.
15
+ class BadOptionError < ArgumentError
16
+ def initialize(option_name, valid_options, received) # :nodoc:
17
+ super("#{option_name.inspect} option must be one of #{valid_options.inspect}, not #{received.inspect}")
18
+ end
19
+ end
6
20
  end
data/lib/ext/logger.rb ADDED
@@ -0,0 +1,7 @@
1
+ unless RUBY_PLATFORM == 'java'
2
+ require 'logger'
3
+ #Need to alias :warn as :warning to match the java logger
4
+ class Logger
5
+ alias :warning :warn
6
+ end
7
+ end
data/lib/gems.yaml ADDED
@@ -0,0 +1,9 @@
1
+ all:
2
+ rack: >=1.0
3
+ builder: >=2.1.2
4
+ warbler: >=0.9.14
5
+ RedCloth: >=4.2.2
6
+ jruby:
7
+ json-jruby: >=1.1.6
8
+ mri:
9
+ json: >=1.2.0
@@ -1,40 +1,72 @@
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
1
+ #Contains the Rack #call method - to be mixed in to the Robot class
2
+ module Rave
3
+ module Mixins
4
+ module Controller
5
+ include Logger
6
+
7
+ def call(env)
8
+ request = Rack::Request.new(env)
9
+ path = request.path_info
10
+ method = request.request_method
11
+ logger.info("#{method}ing #{path}")
12
+ begin
13
+ #There are only 3 URLs that Wave can access:
14
+ # robot capabilities, robot profile, and event notification
15
+ if path == "/_wave/capabilities.xml" && method == "GET"
16
+ [ 200, { 'Content-Type' => 'text/xml' }, capabilities_xml ]
17
+ elsif path == "/_wave/robot/profile" && method == "GET"
18
+ [ 200, { 'Content-Type' => 'application/json' }, profile_json ]
19
+ elsif path == "/_wave/robot/jsonrpc" && method == "POST"
20
+ body = request.body.read
21
+ context, events = parse_json_body(body)
22
+ events.each do |event|
23
+ handle_event(event, context)
24
+ end
25
+ response = context.to_json
26
+ logger.info("Structure (after):\n#{context.print_structure}")
27
+ logger.info("Response:\n#{response}")
28
+ [ 200, { 'Content-Type' => 'application/json' }, response ]
29
+ elsif cron_job = @cron_jobs.find { |job| job[:path] == path }
30
+ body = request.body.read
31
+ context, events = parse_json_body(body)
32
+ self.send(cron_job[:handler], context)
33
+ [ 200, { 'Content-Type' => 'application/json' }, context.to_json ]
34
+ elsif File.exist?(file = File.join(".", "public", *(path.split("/"))))
35
+ #Static resource
36
+ [ 200, { 'Content-Type' => static_resource_content_type(file) }, File.open(file) { |f| f.read } ]
37
+ elsif self.respond_to?(:custom_routes)
38
+ #Let the custom route method defined in the robot take care of the call
39
+ self.custom_routes(request, path, method)
40
+ else
41
+ logger.warning("404 - Not Found: #{path}")
42
+ [ 404, { 'Content-Type' => 'text/html' }, "404 - Not Found" ]
43
+ end
44
+ rescue Exception => e
45
+ logger.warning("500 - Internal Server Error: #{path}")
46
+ logger.warning("#{e.class}: #{e.message}\n\n#{e.backtrace.join("\n")}")
47
+ [ 500, { 'Content-Type' => 'text/html' }, "500 - Internal Server Error"]
48
+ end
49
+ end
50
+
51
+ protected
52
+ def static_resource_content_type(path)
53
+ case (ext = File.extname(path))
54
+ when '.html', '.htm'
55
+ 'text/html'
56
+ when '.xml'
57
+ 'text/xml'
58
+ when '.gif'
59
+ 'image/gif'
60
+ when '.jpeg', '.jpg'
61
+ 'image/jpeg'
62
+ when '.tif', '.tiff'
63
+ 'image/tiff'
64
+ when '.txt', ''
65
+ 'text/plain'
66
+ else
67
+ "application/#{ext}"
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -1,168 +1,206 @@
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(blip_data),
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'] && json['annotations']['list']
100
- json['annotations']['list'].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
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
+ include Logger
6
+
7
+ PROFILE_JAVA_CLASS = 'com.google.wave.api.ParticipantProfile'
8
+
9
+ #Returns this robot's capabilities in XML
10
+ def capabilities_xml
11
+ xml = Builder::XmlMarkup.new
12
+ xml.instruct!
13
+ xml.tag!("w:robot", "xmlns:w" => "http://wave.google.com/extensions/robots/1.0") do
14
+ xml.tag!("w:version", @version)
15
+ xml.tag!("w:capabilities") do
16
+ @handlers.keys.each do |capability|
17
+ xml.tag!("w:capability", "name" => capability)
18
+ end
19
+ end
20
+ unless @cron_jobs.empty?
21
+ xml.tag!("w:crons") do
22
+ @cron_jobs.each do |job|
23
+ xml.tag!("w:cron", "path" => job[:path], "timerinseconds" => job[:seconds])
24
+ end
25
+ end
26
+ end
27
+ attrs = { "name" => @name }
28
+ attrs["imageurl"] = @image_url if @image_url
29
+ attrs["profileurl"] = @profile_url if @profile_url
30
+ xml.tag!("w:profile", attrs)
31
+ end
32
+ end
33
+
34
+ #Returns the robot's profile in json format
35
+ def profile_json
36
+ {
37
+ 'name' => @name,
38
+ 'imageUrl' => @image_url,
39
+ 'profileUrl' => @profile_url,
40
+ 'javaClass' => PROFILE_JAVA_CLASS,
41
+ }.to_json.gsub('\/','/')
42
+ end
43
+
44
+ #Parses context and event info from JSON input
45
+ def parse_json_body(json)
46
+ logger.info("Received:\n#{json.to_s}")
47
+ data = JSON.parse(json)
48
+ #Create Context
49
+ context = context_from_json(data)
50
+ #Create events
51
+ events = events_from_json(data, context)
52
+ logger.info("Structure (before):\n#{context.print_structure}")
53
+ logger.info("Events: #{events.map { |e| e.type }.join(', ')}")
54
+ return context, events
55
+ end
56
+
57
+ protected
58
+ def context_from_json(json)
59
+ blips = {}
60
+ blips_from_json(json).each do |blip|
61
+ blips[blip.id] = blip
62
+ end
63
+ wavelets = {}
64
+ wavelets_from_json(json).each do |wavelet|
65
+ wavelets[wavelet.id] = wavelet
66
+ end
67
+ waves = {}
68
+ #Waves aren't sent back, but we can reconstruct them from the wavelets
69
+ waves_from_wavelets(wavelets).each do |wave|
70
+ waves[wave.id] = wave
71
+ end
72
+ Rave::Models::Context.new(
73
+ :waves => waves,
74
+ :wavelets => wavelets,
75
+ :blips => blips,
76
+ :robot => self
77
+ )
78
+ end
79
+
80
+ def blips_from_json(json)
81
+ map_to_hash(json['blips']).values.collect do |blip_data|
82
+ Rave::Models::Blip.new(
83
+ :id => blip_data['blipId'],
84
+ :annotations => annotations_from_json(blip_data),
85
+ :child_blip_ids => list_to_array(blip_data['childBlipIds']),
86
+ :content => blip_data['content'],
87
+ :contributors => list_to_array(blip_data['contributors']),
88
+ :creator => blip_data['creator'],
89
+ :elements => elements_from_json(blip_data['elements']),
90
+ :last_modified_time => blip_data['lastModifiedTime'],
91
+ :parent_blip_id => blip_data['parentBlipId'],
92
+ :version => blip_data['version'],
93
+ :wave_id => blip_data['waveId'],
94
+ :wavelet_id => blip_data['waveletId']
95
+ )
96
+ end
97
+ end
98
+
99
+ def elements_from_json(elements_map)
100
+ elements = {}
101
+
102
+ map_to_hash(elements_map).each_pair do |position, data|
103
+ elements[position.to_i] = Element.create(data['type'], map_to_hash(data['properties']))
104
+ end
105
+
106
+ elements
107
+ end
108
+
109
+ # Convert a json-java list (which may not be defined) into an array.
110
+ # Defaults to an empty array.
111
+ def list_to_array(list)
112
+ if list.nil?
113
+ []
114
+ else
115
+ list['list'] || []
116
+ end
117
+ end
118
+
119
+ # Convert a json-java map (which may not be defined) into a hash. Defaults
120
+ # to an empty hash.
121
+ def map_to_hash(map)
122
+ if map.nil?
123
+ {}
124
+ else
125
+ map['map'] || {}
126
+ end
127
+ end
128
+
129
+ def annotations_from_json(json)
130
+ list_to_array(json['annotation']).collect do |annotation|
131
+ Rave::Models::Annotation.create(
132
+ annotation['name'],
133
+ annotation['value'],
134
+ range_from_json(annotation['range'])
135
+ )
136
+
137
+ end
138
+ end
139
+
140
+ def range_from_json(json)
141
+ Range.new(json['start'], json['end'])
142
+ end
143
+
144
+ def events_from_json(json, context)
145
+ list_to_array(json['events']).collect do |event|
146
+ properties = {}
147
+ event['properties']['map'].each do |key, value|
148
+ properties[key] = case value
149
+ when String # Just a string, as in blipId.
150
+ value
151
+ when Hash # Serialised array, such as in participantsAdded.
152
+ value['list']
153
+ else
154
+ raise "Unrecognised property #{value} #{value.class}"
155
+ end
156
+ end
157
+ Rave::Models::Event.create(event['type'],
158
+ :timestamp => event['timestamp'],
159
+ :modified_by => event['modifiedBy'],
160
+ :properties => properties,
161
+ :context => context,
162
+ :robot => self
163
+ )
164
+ end
165
+ end
166
+
167
+ def wavelets_from_json(json)
168
+ #Currently only one wavelet is sent back
169
+ #TODO: should this look at the wavelet's children too?
170
+ wavelet = json['wavelet']
171
+ if wavelet
172
+ [
173
+ Rave::Models::Wavelet.new(
174
+ :creator => wavelet['creator'],
175
+ :creation_time => wavelet['creationTime'],
176
+ :data_documents => map_to_hash(wavelet['dataDocuments']),
177
+ :last_modifed_time => wavelet['lastModifiedTime'],
178
+ :participants => list_to_array(wavelet['participants']),
179
+ :root_blip_id => wavelet['rootBlipId'],
180
+ :title => wavelet['title'],
181
+ :version => wavelet['version'],
182
+ :wave_id => wavelet['waveId'],
183
+ :id => wavelet['waveletId']
184
+ )
185
+ ]
186
+ else
187
+ []
188
+ end
189
+ end
190
+
191
+ def waves_from_wavelets(wavelets)
192
+ wave_wavelet_map = {}
193
+ if wavelets
194
+ wavelets.values.each do |wavelet|
195
+ wave_wavelet_map[wavelet.wave_id] ||= []
196
+ wave_wavelet_map[wavelet.wave_id] << wavelet.id
197
+ end
198
+ end
199
+ wave_wavelet_map.collect do |wave_id, wavelet_ids|
200
+ Rave::Models::Wave.new(:id => wave_id, :wavelet_ids => wavelet_ids)
201
+ end
202
+ end
203
+
204
+ end
205
+ end
206
+ end