cosmos 5.0.4 → 5.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -32,12 +32,19 @@ module Cosmos
32
32
  # @return [String] Table configuration filename
33
33
  attr_reader :filename
34
34
 
35
+ def self.process_file(filename)
36
+ instance = self.new()
37
+ instance.process_file(filename)
38
+ instance
39
+ end
40
+
35
41
  # Create the table configuration
36
42
  def initialize
37
43
  super
38
-
39
44
  # Override commands with the Table::TARGET name to store tables
40
45
  @commands[Table::TARGET] = {}
46
+ @definitions = {}
47
+ @last_config = [] # Stores array of [filename, contents]
41
48
  end
42
49
 
43
50
  # @return [Array<Table>] All tables defined in the configuration file
@@ -45,6 +52,11 @@ module Cosmos
45
52
  @commands[Table::TARGET]
46
53
  end
47
54
 
55
+ # @return [String] Table definition for the specific table
56
+ def definition(table_name)
57
+ @definitions[table_name.upcase]
58
+ end
59
+
48
60
  # @return [Array<String>] All the table names
49
61
  def table_names
50
62
  tables.keys
@@ -64,6 +76,7 @@ module Cosmos
64
76
  # Partial files are included into another file and thus aren't directly processed
65
77
  return if File.basename(filename)[0] == '_' # Partials start with underscore
66
78
  @filename = filename
79
+ @last_config = [File.basename(filename), File.read(filename)]
67
80
  @converted_type = nil
68
81
  @converted_bit_size = nil
69
82
  @proc_text = ''
@@ -111,10 +124,12 @@ module Cosmos
111
124
  )
112
125
  end
113
126
  process_file(table_filename)
127
+
114
128
  when 'TABLE'
115
129
  finish_packet
116
130
  @current_packet =
117
131
  TableParser.parse_table(parser, @commands, @warnings)
132
+ @definitions[@current_packet.packet_name] = @last_config
118
133
  @current_cmd_or_tlm = COMMAND
119
134
  @default_index = 0
120
135
 
@@ -31,50 +31,111 @@ module Cosmos
31
31
  class MismatchError < CoreError
32
32
  end
33
33
 
34
- # Raised when there is no current table configuration
35
- class NoConfigError < CoreError
36
- # @return [String] Error message
37
- def message
38
- 'No current table configuration'
39
- end
34
+ def self.binary(binary, definition_filename, table_name)
35
+ config = TableConfig.process_file(definition_filename)
36
+ load_binary(config, binary)
37
+ return config.table(table_name).buffer
40
38
  end
41
39
 
42
- # Raised when there is no table in the current configuration
43
- class NoTableError < CoreError
44
- # @return [String] Error message
45
- def message
46
- 'Table does not exist in current configuration'
47
- end
40
+ def self.definition(definition_filename, table_name)
41
+ config = TableConfig.process_file(definition_filename)
42
+ return config.definition(table_name) # This returns an array: [filename, contents]
48
43
  end
49
44
 
50
- # @return [TableConfig] Configuration instance
51
- attr_reader :config
45
+ def self.report(binary, definition_filename, requested_table_name = nil)
46
+ report = StringIO.new
47
+ config = TableConfig.process_file(definition_filename)
48
+ begin
49
+ load_binary(config, binary)
50
+ rescue CoreError => err
51
+ report.puts "Error: #{err.message}\n"
52
+ end
53
+
54
+ config.tables.each do |table_name, table|
55
+ next if requested_table_name && table_name != requested_table_name
56
+ items = table.sorted_items
57
+ report.puts(table.table_name)
52
58
 
53
- # Create the instance
54
- def initialize
55
- reset
59
+ # Write the column headers
60
+ if table.type == :ROW_COLUMN
61
+ columns = ['Item']
62
+
63
+ # Remove the '0' from the 'itemname0'
64
+ table.num_columns.times.each do |x|
65
+ columns << items[x].name[0...-1]
66
+ end
67
+ report.puts columns.join(', ')
68
+ else
69
+ report.puts 'Label, Value'
70
+ end
71
+
72
+ # Write the table item values
73
+ (0...table.num_rows).each do |r|
74
+ if table.type == :ROW_COLUMN
75
+ rowtext = "#{r + 1}"
76
+ else
77
+ rowtext = items[r].name
78
+ end
79
+
80
+ report.write "#{rowtext}, "
81
+ (0...table.num_columns).each do |c|
82
+ if table.type == :ROW_COLUMN
83
+ table_item = items[c + r * table.num_columns]
84
+ else
85
+ table_item = items[r]
86
+ end
87
+ value = table.read(table_item.name, :FORMATTED)
88
+ if value.is_printable?
89
+ report.write "#{value}, "
90
+ else
91
+ report.write "#{value.simple_formatted}, "
92
+ end
93
+ end
94
+ report.write("\n") # newline after each row
95
+ end
96
+ report.write("\n") # newline after each table
97
+ end
98
+ report.string
56
99
  end
57
100
 
58
- # Clears the configuration
59
- def reset
60
- @config = nil
101
+ def self.generate(definition_filename)
102
+ config = TableConfig.process_file(definition_filename)
103
+ binary = ''
104
+ config.tables.each do |table_name, table|
105
+ table.restore_defaults
106
+ binary += table.buffer
107
+ end
108
+ binary
61
109
  end
62
110
 
63
- # @param filename [String] Create a new TableConfig instance and process the filename
64
- def process_definition(filename)
65
- @config = TableConfig.new
66
- @config.process_file(filename)
111
+ def self.save(definition_filename, tables)
112
+ config = TableConfig.process_file(definition_filename)
113
+ tables.each do |table|
114
+ table_def = config.tables[table['name']]
115
+ table['rows'].each do |row|
116
+ row.each do |item|
117
+ # TODO: I don't know how the frontend could edit an item like this:
118
+ # item:{"name"=>"BINARY", "value"=>{"json_class"=>"String", "raw"=>[222, 173, 190, 239]} }
119
+ next if item['value'].is_a? Hash
120
+ table_def.write(item['name'], item['value'])
121
+ end
122
+ end
123
+ end
124
+ binary = ''
125
+ config.tables.each { |table_name, table| binary += table.buffer }
126
+ binary
67
127
  end
68
128
 
69
- def generate_json(bin_path, def_path)
129
+ def self.build_json(binary, definition_filename)
130
+ config = TableConfig.process_file(definition_filename)
70
131
  tables = []
71
132
  json = { tables: tables }
72
133
  begin
73
- file_open(bin_path, def_path)
134
+ load_binary(config, binary)
74
135
  rescue CoreError => err
75
136
  json['errors'] = err.message
76
137
  end
77
- @config.tables.each do |table_name, table|
138
+ config.tables.each do |table_name, table|
78
139
  tables << {
79
140
  name: table_name,
80
141
  numRows: table.num_rows,
@@ -129,301 +190,144 @@ module Cosmos
129
190
  json.to_json
130
191
  end
131
192
 
132
- def save_tables(bin_path, def_path, tables)
133
- file_open(bin_path, def_path)
134
- tables.each do |table|
135
- table_def = @config.tables[table['name']]
136
- table['rows'].each do |row|
137
- row.each do |item|
138
- # TODO: I don't know how the frontend could edit an item like this:
139
- # item:{"name"=>"BINARY", "value"=>{"json_class"=>"String", "raw"=>[222, 173, 190, 239]} }
140
- next if item['value'].is_a? Hash
141
- table_def.write(item['name'], item['value'])
142
- end
143
- end
144
- end
145
- file_save(bin_path)
146
- bin_path
147
- end
148
-
149
- # @param def_path [String] Definition file to process
150
- # @param output_dir [String] Output directory to create the new file
151
- # @return [String] Binary file path
152
- def file_new(def_path, output_dir)
153
- process_definition(def_path)
154
- @config.table_names.each do |table_name|
155
- set_binary_data_to_default(table_name)
156
- end
157
- bin_path = File.join(output_dir, def_to_bin_filename(def_path))
158
- file_save(bin_path)
159
- bin_path
160
- end
161
-
162
- # @param bin_path [String] Binary file to open
163
- # @param def_path [String] Definition file to use when opening
164
- def file_open(bin_path, def_path)
165
- process_definition(def_path)
166
- open_and_load_binary_file(bin_path)
167
- end
168
-
169
- # Saves the current tables in the config instance to the given filename.
170
- #
171
- # @param filename [String] Filename to write, overwritten if it exists.
172
- def file_save(filename)
173
- raise NoConfigError unless @config
174
- file_check
175
- File.open(filename, 'wb') do |file|
176
- @config.tables.each { |table_name, table| file.write(table.buffer) }
177
- end
178
- # file_report(filename, @config.filename)
179
- end
180
-
181
- # @return [String] Success string if parameters all check. Raises
182
- # a CoreError if errors are found.
183
- def file_check
184
- raise NoConfigError unless @config
185
- result = ''
186
- @config.table_names.each do |name|
187
- table_result = table_check(name)
188
- unless table_result.empty?
189
- result << "Errors in #{name}:\n" + table_result
190
- end
191
- end
192
- raise CoreError, result unless result.empty?
193
- 'All parameters are within their constraints.'
194
- end
195
-
196
- # Create a CSV report file based on the file contents.
197
- #
198
- # @param bin_path [String] Binary filename currently open. Used to generate the
199
- # report name such that it matches the binary filename.
200
- # @param def_path [String] Definition filename currently open
201
- # @return [String] Report filename path
202
- def file_report(bin_path, def_path)
203
- raise NoConfigError unless @config
204
- file_check
205
-
206
- basename = File.basename(bin_path, '.bin')
207
- report_path = File.join(File.dirname(bin_path), "#{basename}.csv")
208
- File.open(report_path, 'w+') do |file|
209
- file.write("File Definition, #{def_path}\n")
210
- file.write("File Binary, #{bin_path}\n\n")
211
- @config.tables.values.each do |table|
212
- items = table.sorted_items
213
- file.puts(table.table_name)
214
-
215
- # Write the column headers
216
- if table.type == :ROW_COLUMN
217
- columns = ['Item']
218
-
219
- # Remove the '0' from the 'itemname0'
220
- table.num_columns.times.each do |x|
221
- columns << items[x].name[0...-1]
222
- end
223
- file.puts columns.join(', ')
224
- else
225
- file.puts 'Label, Value'
226
- end
227
-
228
- # Write the table item values
229
- (0...table.num_rows).each do |r|
230
- if table.type == :ROW_COLUMN
231
- rowtext = "#{r + 1}"
232
- else
233
- rowtext = items[r].name
234
- end
235
-
236
- file.write "#{rowtext}, "
237
- (0...table.num_columns).each do |c|
238
- if table.type == :ROW_COLUMN
239
- table_item = items[c + r * table.num_columns]
240
- else
241
- table_item = items[r]
242
- end
243
-
244
- file.write "#{table.read(table_item.name, :FORMATTED).to_s}, "
245
- end
246
- file.write("\n") # newline after each row
247
- end
248
- file.write("\n") # newline after each table
249
- end
250
- end
251
- report_path
252
- end
253
-
254
- # Create a hex formatted string of all the file data
255
- def file_hex
256
- raise NoConfigError unless @config
257
- data = ''
258
- @config.tables.values.each { |table| data << table.buffer }
259
- "#{data.formatted}\n\nTotal Bytes Read: #{data.length}"
260
- end
261
-
262
- # @param table_name [String] Name of the table to check for out of range values
263
- def table_check(table_name)
264
- raise NoConfigError unless @config
265
- table = @config.table(table_name)
266
- raise NoTableError unless table
267
-
268
- result = ''
269
- table_items = table.sorted_items
270
-
271
- # Check the ranges and constraints for each item in the table
272
- # We go through it this way (by row and columns) so we can grab the actual
273
- # user input when we display any errors found
274
- (0...table.num_rows).each do |r|
275
- (0...table.num_columns).each do |c|
276
- # get the table item definition so we know how to save it
277
- table_item = table_items[r * table.num_columns + c]
278
-
279
- value = table.read(table_item.name)
280
- unless table_item.range.nil?
281
- # If the item has states which include the value, then convert
282
- # the state back to the numeric value for range checking
283
- if table_item.states && table_item.states.include?(value)
284
- value = table_item.states[value]
285
- end
286
-
287
- # check to see if the value lies within its valid range
288
- unless table_item.range.include?(value)
289
- if table_item.format_string
290
- value = table.read(table_item.name, :FORMATTED)
291
- range_first =
292
- sprintf(table_item.format_string, table_item.range.first)
293
- range_last =
294
- sprintf(table_item.format_string, table_item.range.last)
295
- else
296
- range_first = table_item.range.first
297
- range_last = table_item.range.last
298
- end
299
- result <<
300
- " #{table_item.name}: #{value} outside valid range of #{range_first}..#{range_last}\n"
301
- end
302
- end
303
- end # end each column
304
- end # end each row
305
- result
306
- end
307
-
308
- # @param table_name [String] Name of the table to revert all values to default
309
- def table_default(table_name)
310
- raise NoConfigError unless @config
311
- set_binary_data_to_default(table_name)
312
- end
313
-
314
- # @param table_name [String] Create a hex formatted string of the given table data
315
- def table_hex(table_name)
316
- raise NoConfigError unless @config
317
- table = @config.table(table_name)
318
- raise NoTableError unless table
319
- "#{table.buffer.formatted}\n\nTotal Bytes Read: #{table.buffer.length}"
320
- end
321
-
322
- # @param table_name [String] Table name to write as a stand alone file
323
- # @param filename [String] Filename to write the table data to. Existing
324
- # files will be overwritten.
325
- def table_save(table_name, filename)
326
- raise NoConfigError unless @config
327
- result = table_check(table_name)
328
- unless result.empty?
329
- raise CoreError, "Errors in #{table_name}:\n#{result}"
330
- end
331
- File.open(filename, 'wb') do |file|
332
- file.write(@config.table(table_name).buffer)
333
- end
334
- end
335
-
336
- # Commit a table from the current configuration into a new binary
337
- #
338
- # @param table_name [String] Table name to commit to an existing binary
339
- # @param bin_file [String] Binary file to open
340
- # @param def_file [String] Definition file to use when opening
341
- def table_commit(table_name, bin_file, def_file)
342
- raise NoConfigError unless @config
343
- save_table = @config.table(table_name)
344
- raise NoTableError unless save_table
345
-
346
- result = table_check(table_name)
347
- unless result.empty?
348
- raise CoreError, "Errors in #{table_name}:\n#{result}"
349
- end
350
-
351
- config = TableConfig.new
352
- begin
353
- config.process_file(def_file)
354
- rescue => err
355
- raise CoreError,
356
- "The table definition file:#{def_file} has the following errors:\n#{err}"
357
- end
358
-
359
- if !config.table_names.include?(table_name.upcase)
360
- raise NoTableError,
361
- "#{table_name} not found in #{def_file} table definition file."
362
- end
363
-
364
- saved_config = @config
365
- @config = config
366
- open_and_load_binary_file(bin_file)
367
-
368
- # Store the saved table data in the new table definition
369
- table = config.table(save_table.table_name)
370
- table.buffer = save_table.buffer[0...table.length]
371
- file_save(bin_file)
372
- @config = saved_config
373
- end
374
-
375
- protected
376
-
377
- # Set all the binary data in the table to the default values
378
- def set_binary_data_to_default(table_name)
379
- table = @config.table(table_name)
380
- raise NoTableError unless table
381
- table.restore_defaults
382
- end
383
-
384
- # Get the binary filename equivalent for the given definition filename
385
- def def_to_bin_filename(def_path)
386
- if File.basename(def_path) =~ /_def\.txt$/
387
- # Remove _def.txt if present (should be)
388
- basename = File.basename(def_path)[0...-8]
389
- else
390
- # Remove any extension if present
391
- basename = File.basename(def_path, File.extname(def_path))
392
- end
393
- "#{basename}.bin"
394
- end
395
-
396
- # Opens the given binary file and populates the table definition.
397
- # The filename parameter should be a properly formatted file path.
398
- def open_and_load_binary_file(filename)
399
- begin
400
- data = nil
401
-
402
- # read the binary file and store it into an array
403
- File.open(filename, 'rb') { |file| data = file.read }
404
- rescue => err
405
- raise "Unable to open and load #{filename} due to #{err}."
406
- end
407
-
193
+ def self.load_binary(config, data)
408
194
  binary_data_index = 0
409
195
  total_table_length = 0
410
- @config.tables.each do |table_name, table|
196
+ config.tables.each do |table_name, table|
411
197
  total_table_length += table.length
412
198
  end
413
- @config.tables.each do |table_name, table|
199
+ config.tables.each do |table_name, table|
414
200
  if binary_data_index + table.length > data.length
415
201
  table.buffer = data[binary_data_index..-1]
416
202
  raise MismatchError,
417
- "Binary size of #{data.length} not large enough to fully represent table definition of length #{total_table_length}. The remaining table definition (starting with byte #{data.length - binary_data_index} in #{table.table_name}) will be filled with 0."
203
+ "Binary size of #{data.length} not large enough to fully represent table definition of length #{total_table_length}. "+
204
+ "The remaining table definition (starting with byte #{data.length - binary_data_index} in #{table.table_name}) will be filled with 0."
418
205
  end
419
- table.buffer =
420
- data[binary_data_index...binary_data_index + table.length]
206
+ table.buffer = data[binary_data_index...binary_data_index + table.length]
421
207
  binary_data_index += table.length
422
208
  end
423
209
  if binary_data_index < data.length
424
210
  raise MismatchError,
425
- "Binary size of #{data.length} larger than table definition of length #{total_table_length}. Discarding the remaing #{data.length - binary_data_index} bytes."
211
+ "Binary size of #{data.length} larger than table definition of length #{total_table_length}. "+
212
+ "Discarding the remaing #{data.length - binary_data_index} bytes."
426
213
  end
427
214
  end
215
+
216
+ # TODO: Potentially useful methods?
217
+ # # @return [String] Success string if parameters all check. Raises
218
+ # # a CoreError if errors are found.
219
+ # def file_check
220
+ # raise NoConfigError unless @config
221
+ # result = ''
222
+ # @config.table_names.each do |name|
223
+ # table_result = table_check(name)
224
+ # unless table_result.empty?
225
+ # result << "Errors in #{name}:\n" + table_result
226
+ # end
227
+ # end
228
+ # raise CoreError, result unless result.empty?
229
+ # 'All parameters are within their constraints.'
230
+ # end
231
+
232
+ # # Create a hex formatted string of all the file data
233
+ # def file_hex
234
+ # raise NoConfigError unless @config
235
+ # data = ''
236
+ # @config.tables.values.each { |table| data << table.buffer }
237
+ # "#{data.formatted}\n\nTotal Bytes Read: #{data.length}"
238
+ # end
239
+
240
+ # # @param table_name [String] Name of the table to check for out of range values
241
+ # def table_check(table_name)
242
+ # raise NoConfigError unless @config
243
+ # table = @config.table(table_name)
244
+ # raise NoTableError unless table
245
+
246
+ # result = ''
247
+ # table_items = table.sorted_items
248
+
249
+ # # Check the ranges and constraints for each item in the table
250
+ # # We go through it this way (by row and columns) so we can grab the actual
251
+ # # user input when we display any errors found
252
+ # (0...table.num_rows).each do |r|
253
+ # (0...table.num_columns).each do |c|
254
+ # # get the table item definition so we know how to save it
255
+ # table_item = table_items[r * table.num_columns + c]
256
+
257
+ # value = table.read(table_item.name)
258
+ # unless table_item.range.nil?
259
+ # # If the item has states which include the value, then convert
260
+ # # the state back to the numeric value for range checking
261
+ # if table_item.states && table_item.states.include?(value)
262
+ # value = table_item.states[value]
263
+ # end
264
+
265
+ # # check to see if the value lies within its valid range
266
+ # unless table_item.range.include?(value)
267
+ # if table_item.format_string
268
+ # value = table.read(table_item.name, :FORMATTED)
269
+ # range_first =
270
+ # sprintf(table_item.format_string, table_item.range.first)
271
+ # range_last =
272
+ # sprintf(table_item.format_string, table_item.range.last)
273
+ # else
274
+ # range_first = table_item.range.first
275
+ # range_last = table_item.range.last
276
+ # end
277
+ # result <<
278
+ # " #{table_item.name}: #{value} outside valid range of #{range_first}..#{range_last}\n"
279
+ # end
280
+ # end
281
+ # end # end each column
282
+ # end # end each row
283
+ # result
284
+ # end
285
+
286
+ # # @param table_name [String] Create a hex formatted string of the given table data
287
+ # def table_hex(table_name)
288
+ # raise NoConfigError unless @config
289
+ # table = @config.table(table_name)
290
+ # raise NoTableError unless table
291
+ # "#{table.buffer.formatted}\n\nTotal Bytes Read: #{table.buffer.length}"
292
+ # end
293
+
294
+ # # Commit a table from the current configuration into a new binary
295
+ # #
296
+ # # @param table_name [String] Table name to commit to an existing binary
297
+ # # @param bin_file [String] Binary file to open
298
+ # # @param def_file [String] Definition file to use when opening
299
+ # def table_commit(table_name, bin_file, def_file)
300
+ # raise NoConfigError unless @config
301
+ # save_table = @config.table(table_name)
302
+ # raise NoTableError unless save_table
303
+
304
+ # result = table_check(table_name)
305
+ # unless result.empty?
306
+ # raise CoreError, "Errors in #{table_name}:\n#{result}"
307
+ # end
308
+
309
+ # config = TableConfig.new
310
+ # begin
311
+ # config.process_file(def_file)
312
+ # rescue => err
313
+ # raise CoreError,
314
+ # "The table definition file:#{def_file} has the following errors:\n#{err}"
315
+ # end
316
+
317
+ # if !config.table_names.include?(table_name.upcase)
318
+ # raise NoTableError,
319
+ # "#{table_name} not found in #{def_file} table definition file."
320
+ # end
321
+
322
+ # saved_config = @config
323
+ # @config = config
324
+ # open_and_load_binary_file(bin_file)
325
+
326
+ # # Store the saved table data in the new table definition
327
+ # table = config.table(save_table.table_name)
328
+ # table.buffer = save_table.buffer[0...table.length]
329
+ # file_save(bin_file)
330
+ # @config = saved_config
331
+ # end
428
332
  end
429
333
  end
@@ -0,0 +1,68 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2022 Ball Aerospace & Technologies Corp.
4
+ # All Rights Reserved.
5
+ #
6
+ # This program is free software; you can modify and/or redistribute it
7
+ # under the terms of the GNU Affero General Public License
8
+ # as published by the Free Software Foundation; version 3 with
9
+ # attribution addendums as found in the LICENSE.txt
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Affero General Public License for more details.
15
+ #
16
+ # This program may also be used under the terms of a commercial or
17
+ # enterprise edition license of COSMOS if purchased from the
18
+ # copyright holder
19
+
20
+ require 'cosmos/topics/topic'
21
+
22
+ module Cosmos
23
+ class ConfigTopic < Topic
24
+ PRIMARY_KEY = "__CONFIG"
25
+
26
+ # Helper method to initialize the stream and ensure a consistent key
27
+ def self.initialize_stream(scope)
28
+ self.initialize_streams(["#{scope}#{PRIMARY_KEY}"])
29
+ end
30
+
31
+ # Write a configuration change to the topic
32
+ # @param config [Hash] Hash with required keys 'kind', 'name', 'type'
33
+ def self.write(config, scope:)
34
+ unless config.keys.include?(:kind)
35
+ raise "ConfigTopic error, required key kind: not given"
36
+ end
37
+ unless ['created', 'deleted'].include?(config[:kind])
38
+ raise "ConfigTopic error unknown kind: #{config[:kind]}"
39
+ end
40
+ unless config.keys.include?(:name)
41
+ raise "ConfigTopic error, required key name: not given"
42
+ end
43
+ unless config.keys.include?(:type)
44
+ raise "ConfigTopic error, required key type: not given"
45
+ end
46
+ # Limit the configuration topics to 1000 entries
47
+ Topic.write_topic("#{scope}#{PRIMARY_KEY}", config, '*', 1000)
48
+ end
49
+
50
+ def self.read(offset = nil, count: 100, scope:)
51
+ topic = "#{scope}#{PRIMARY_KEY}"
52
+ if offset
53
+ result = Topic.read_topics([topic], [offset], nil, count)
54
+ if result.empty?
55
+ [] # We want to return an empty array rather than an empty hash
56
+ else
57
+ # result is a hash with the topic key followed by an array of results
58
+ # This returns just the array of arrays [[offset, hash], [offset, hash], ...]
59
+ result[topic]
60
+ end
61
+ else
62
+ result = Topic.get_newest_message(topic)
63
+ return [result] if result
64
+ return []
65
+ end
66
+ end
67
+ end
68
+ end