athena 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -2,12 +2,22 @@
2
2
 
3
3
  == VERSION
4
4
 
5
- This documentation refers to athena version 0.2.1
5
+ This documentation refers to athena version 0.2.2
6
6
 
7
7
 
8
8
  == DESCRIPTION
9
9
 
10
- TODO: well, the description... ;-)
10
+ Athena is a library to convert "database" files between various formats.
11
+ It's accompanied by a corresponding executable that gives access to its
12
+ converting features from the command-line. See Athena::Formats for the
13
+ supported database formats and how you can add your own.
14
+
15
+ === Plugins
16
+
17
+ Athena has a plugin system that allows you to implement additional database
18
+ formats and have them available easily. Just create a file +athena_plugin.rb+
19
+ (Athena::PLUGIN_FILENAME) in your Gem's +lib+ directory or any directory that's
20
+ placed in <tt>$LOAD_PATH</tt>. See Athena::Formats on how to define custom formats.
11
21
 
12
22
 
13
23
  == LINKS
data/Rakefile CHANGED
@@ -14,7 +14,7 @@ begin
14
14
  :summary => %q{Convert database files to various formats.},
15
15
  :author => %q{Jens Wille},
16
16
  :email => %q{jens.wille@uni-koeln.de},
17
- :dependencies => %w[builder xmlstreamin] << ['ruby-nuggets', '>= 0.7.4']
17
+ :dependencies => %w[builder xmlstreamin] << ['ruby-nuggets', '>= 0.8.1']
18
18
  }
19
19
  }}
20
20
  rescue LoadError => err
data/bin/athena CHANGED
@@ -1,4 +1,4 @@
1
- #! /usr/bin/ruby
1
+ #! /usr/bin/env ruby
2
2
 
3
3
  #--
4
4
  ###############################################################################
@@ -0,0 +1,13 @@
1
+ class Bla < Athena::Formats::Base
2
+
3
+ def parse
4
+ # ...
5
+ end
6
+
7
+ def convert
8
+ # ...
9
+ end
10
+
11
+ register_format :blub
12
+
13
+ end
@@ -26,55 +26,83 @@
26
26
  ###############################################################################
27
27
  #++
28
28
 
29
- # Athena is a library to convert (mainly) prometheus database files to various
30
- # output formats. It's accompanied by a corresponding script that gives access
31
- # to all its converting features.
32
- #
33
- # In order to support additional input and/or output formats, Athena::Formats::Base
34
- # needs to be sub-classed and, respectively, an instance method _parse_ or an
35
- # instance method _convert_ supplied. This way, a specific format can even function
36
- # as both input and output format.
37
-
38
29
  require 'athena/version'
39
30
 
31
+ # See README.
32
+
40
33
  module Athena
41
34
 
42
- autoload :Parser, 'athena/parser'
43
35
  autoload :Record, 'athena/record'
44
36
  autoload :Formats, 'athena/formats'
45
37
 
46
38
  extend self
47
39
 
48
- def parser(config, format)
49
- Parser.new(config, format)
50
- end
40
+ DEFAULT_SEPARATOR = ', '
41
+ DEFAULT_EMPTY = '<<EMPTY>>'
51
42
 
52
- def input_formats
53
- Formats::Base.formats[:in].sort
43
+ def run(config, spec, format, input, output)
44
+ Formats[:out, format, output].run(
45
+ Formats[:in, spec, build_config(config)],
46
+ input
47
+ )
54
48
  end
55
49
 
56
- def valid_input_format?(format)
57
- Formats::Base.valid_format?(:in, format)
50
+ def input_formats
51
+ Formats.formats[:in].sort
58
52
  end
59
53
 
60
54
  def output_formats
61
- Formats::Base.formats[:out].sort
55
+ Formats.formats[:out].sort
62
56
  end
63
57
 
64
- def valid_output_format?(format)
65
- Formats::Base.valid_format?(:out, format)
58
+ def valid_format?(direction, format)
59
+ Formats.valid_format?(direction, format)
66
60
  end
67
61
 
68
- def deferred_output?(format)
69
- Formats[:out, format].deferred?
62
+ def valid_input_format?(format)
63
+ valid_format?(:in, format)
70
64
  end
71
65
 
72
- def raw_output?(format)
73
- Formats[:out, format].raw?
66
+ def valid_output_format?(format)
67
+ valid_format?(:out, format)
74
68
  end
75
69
 
76
- def with_format(format, *args, &block)
77
- Formats[:out, format].wrap(*args, &block)
70
+ private
71
+
72
+ def build_config(config)
73
+ hash = {}
74
+
75
+ config.each { |field, value|
76
+ if field.to_s =~ /\A__/
77
+ hash[field] = value
78
+ else
79
+ case value
80
+ when String, Array
81
+ elements, value = [*value], {}
82
+ when Hash
83
+ elements = value[:elements] || value[:element].to_a
84
+
85
+ raise ArgumentError, "no elements specified for field #{field}" unless elements.is_a?(Array)
86
+ else
87
+ raise ArgumentError, "illegal value for field #{field}"
88
+ end
89
+
90
+ separator = value[:separator] || DEFAULT_SEPARATOR
91
+
92
+ elements.each { |element|
93
+ (hash[element] ||= {})[field] = {
94
+ :string => value[:string] || ['%s'] * elements.size * separator,
95
+ :empty => value[:empty] || DEFAULT_EMPTY,
96
+ :elements => elements
97
+ }
98
+ }
99
+ end
100
+ }
101
+
102
+ hash
78
103
  end
79
104
 
80
105
  end
106
+
107
+ require 'nuggets/util/pluggable'
108
+ Util::Pluggable.load_plugins_for(Athena)
@@ -63,42 +63,18 @@ module Athena
63
63
  }.reverse.find { |t| config[target = t.to_sym] }
64
64
  end or abort "Config not found for target: #{target}."
65
65
 
66
- parser = Athena.parser(target_config, spec)
67
-
68
66
  input = options[:input]
69
67
  input = arguments.shift unless input != defaults[:input] || arguments.empty?
70
- options[:input] = File.directory?(input) ? Dir.open(input) : open_file_or_std(input)
68
+ input = File.directory?(input) ? Dir.open(input) : open_file_or_std(input)
71
69
 
72
70
  quit unless arguments.empty?
73
71
 
74
- options[:output] = open_file_or_std(options[:output], true)
75
-
76
- if Athena.deferred_output?(format)
77
- res = parser.parse(options[:input])
78
-
79
- res.map { |record|
80
- record.to(format)
81
- }.flatten.sort.uniq.each { |line|
82
- options[:output].puts line
83
- }
84
- elsif Athena.raw_output?(format)
85
- res = Athena.with_format(format, options[:output]) { |_format|
86
- parser.parse(options[:input]) { |record|
87
- record.to(_format)
88
- }
89
- }
90
- else
91
- res = Athena.with_format(format) { |_format|
92
- parser.parse(options[:input]) { |record|
93
- options[:output].puts record.to(_format)
94
- }
95
- }
96
- end
72
+ Athena.run(target_config, spec, format, input, open_file_or_std(options[:output], true))
97
73
  end
98
74
 
99
75
  private
100
76
 
101
- def merge_config(args = [default])
77
+ def merge_config(args = [defaults])
102
78
  super
103
79
  end
104
80
 
@@ -124,13 +100,7 @@ module Athena
124
100
  }
125
101
 
126
102
  opts.on('-L', '--list-specs', "List available input formats (specs) and exit") {
127
- puts 'Available input formats (specs):'
128
-
129
- Athena.input_formats.each { |format, name|
130
- puts " - #{format}#{" (= #{name})" if format != name.to_s}"
131
- }
132
-
133
- exit
103
+ print_formats(:in)
134
104
  }
135
105
 
136
106
  opts.separator ''
@@ -145,13 +115,7 @@ module Athena
145
115
  }
146
116
 
147
117
  opts.on('-l', '--list-formats', "List available output formats and exit") {
148
- puts 'Available output formats:'
149
-
150
- Athena.output_formats.each { |format, name|
151
- puts " - #{format}#{" (= #{name})" if format != name.to_s}"
152
- }
153
-
154
- exit
118
+ print_formats(:out)
155
119
  }
156
120
 
157
121
  opts.separator ''
@@ -161,6 +125,19 @@ module Athena
161
125
  }
162
126
  end
163
127
 
128
+ def print_formats(direction)
129
+ puts "Available #{direction}put formats:"
130
+
131
+ Athena.send("#{direction}put_formats").each { |name, klass|
132
+ line, format = " - #{name}", klass.format
133
+ line << " (= #{format})" if name != format && Athena.valid_format?(direction, format)
134
+
135
+ puts line
136
+ }
137
+
138
+ exit
139
+ end
140
+
164
141
  end
165
142
 
166
143
  end
@@ -30,108 +30,416 @@ require 'athena'
30
30
 
31
31
  module Athena
32
32
 
33
+ # In order to support additional input and/or output formats,
34
+ # Athena::Formats::Base needs to be sub-classed and an instance
35
+ # method _parse_ or an instance method _convert_ supplied,
36
+ # respectively. This way, a specific format can even function
37
+ # as both input and output format.
38
+ #
39
+ # == Defining custom formats
40
+ #
41
+ # Define one or more classes that inherit from Athena::Formats::Base
42
+ # and either call Athena::Formats.register with your format class as
43
+ # parameter or call Athena::Formats.register_all with your surrounding
44
+ # namespace (which will then recursively add any format definitions below
45
+ # it). Alternatively, you can call Athena::Formats::Base.register_format
46
+ # <b>at the end</b> of your class definition to register just this class.
47
+ #
48
+ # The directions supported by your custom format are determined
49
+ # automatically; see below for further details.
50
+ #
51
+ # === Defining an _input_ format
52
+ #
53
+ # An input format must provide a *public* instance method _parse_ that
54
+ # accepts an input object (usually an IO object) and a block it passes
55
+ # each record (Athena::Record) to. See Athena::Formats::Base#parse.
56
+ #
57
+ # === Defining an _output_ format
58
+ #
59
+ # An output format must provide a *public* instance method _convert_ that
60
+ # accepts a record (Athena::Record) and either returns a suitable value for
61
+ # output or writes the output itself. See Athena::Formats::Base#convert.
62
+ #
63
+ # == Aliases
64
+ #
65
+ # In order to provide an alias name for a format, simply assign
66
+ # the format class to a new constant. Then you need to call
67
+ # <tt>Athena::Formats.register(your_new_const)</tt> to register
68
+ # your alias.
69
+
33
70
  module Formats
34
71
 
35
- CRLF = %Q{\015\012}
72
+ # CR+LF line ending.
73
+ CRLF = %Q{\015\012}
74
+
75
+ # Regular expression to match (multiple) CR+LF line endings.
36
76
  CRLF_RE = %r{(?:\r?\n)+}
37
77
 
38
- def self.[](direction, format)
39
- if direction == :out
40
- if format.class < Base
41
- if format.class.direction != direction
42
- raise DirectionMismatchError,
43
- "expected #{direction}, got #{format.class.direction}"
78
+ # Mapping of format direction to method required for implementation.
79
+ METHODS = { :in => 'parse', :out => 'convert' }
80
+
81
+ @formats = { :in => {}, :out => {} }
82
+
83
+ class << self
84
+
85
+ # Container for registered formats per direction. Use ::find or ::[]
86
+ # to access them.
87
+ attr_reader :formats
88
+
89
+ # call-seq:
90
+ # Athena::Formats[direction, format, *args] -> aFormat
91
+ #
92
+ # Retrieves the format for +direction+ by its name +format+ (see
93
+ # ::find) and initializes it with +args+ (see Base#init). Returns
94
+ # +format+ unaltered if it already is a format instance, while
95
+ # making sure that it supports +direction+.
96
+ def [](direction, format, *args)
97
+ res = find(direction, format, true)
98
+ res.is_a?(Base) ? res : res.init(direction, *args)
99
+ end
100
+
101
+ # call-seq:
102
+ # Athena::Formats.find(direction, format) -> aFormatClass
103
+ #
104
+ # Retrieves the format for +direction+ by its name +format+. Returns
105
+ # +format+'s class if it already is a format instance, while making
106
+ # sure that it supports +direction+.
107
+ def find(direction, format, instance = false)
108
+ directions = formats.keys
109
+
110
+ unless directions.include?(direction)
111
+ raise ArgumentError, "invalid direction: #{direction.inspect}" <<
112
+ " (must be one of: #{directions.map { |d| d.inspect }.join(', ')})"
113
+ end
114
+
115
+ case format
116
+ when Symbol
117
+ find(direction, format.to_s)
118
+ when String
119
+ formats[direction][format] or
120
+ raise FormatNotFoundError.new(direction, format)
44
121
  else
45
- format
46
- end
122
+ klass = format.class
123
+
124
+ if klass < Base && !(directions = klass.directions).empty?
125
+ if klass.has_direction?(direction)
126
+ return instance ? format : klass
127
+ else
128
+ raise DirectionMismatchError.new(direction, directions)
129
+ end
130
+ else
131
+ raise ArgumentError, "invalid format of type #{klass}" <<
132
+ " (expected one of: Symbol, String, or sub-class of #{Base})"
133
+ end
134
+ end
135
+ end
136
+
137
+ # call-seq:
138
+ # Athena::Formats.valid_format?(direction, format) -> true | false
139
+ #
140
+ # Indicates whether the +direction+/+format+ combination is supported,
141
+ # i.e. a format by name +format+ has been registered and supports
142
+ # +direction+.
143
+ def valid_format?(direction, format)
144
+ if format.class < Base
145
+ format.class.has_direction?(direction)
47
146
  else
48
- Base.formats[direction][format].new
147
+ formats[direction].has_key?(format.to_s)
148
+ end
149
+ end
150
+
151
+ # call-seq:
152
+ # Athena::Formats.register(klass, name = nil, relax = false) -> anArray | nil
153
+ #
154
+ # Registers +klass+ as format under +name+ (defaults to Base.format).
155
+ # Only warns instead of raising any errors when +relax+ is +true+.
156
+ # Returns an array of the actual name +klass+ has been registered
157
+ # under and the directions supported; returns +nil+ if nothing has
158
+ # been registered.
159
+ def register(klass, name = nil, relax = false)
160
+ unless klass < Base
161
+ return if relax
162
+ raise ArgumentError, "must be a sub-class of #{Base}"
49
163
  end
50
- else
51
- Base.formats[direction][format]
164
+
165
+ name = name ? name.to_s : klass.format
166
+ methods = klass.public_instance_methods(false).map { |m| m.to_s }
167
+ directions = klass.directions
168
+
169
+ METHODS.each { |direction, method|
170
+ next unless methods.include?(method)
171
+
172
+ if formats[direction].has_key?(name)
173
+ err = DuplicateFormatDefinitionError.new(direction, name)
174
+ raise err unless relax
175
+
176
+ warn err
177
+ next
178
+ else
179
+ directions << direction unless klass.has_direction?(direction)
180
+ formats[direction][name] = klass
181
+ end
182
+ }
183
+
184
+ [name, directions] unless directions.empty?
185
+ end
186
+
187
+ # call-seq:
188
+ # Athena::Formats.register_all(klass = self) -> anArray
189
+ #
190
+ # Recursively registers all formats *below* +klass+ (see ::register).
191
+ # Returns an array of all registered format names with their supported
192
+ # directions.
193
+ def register_all(klass = self, registered = [])
194
+ names = klass.constants
195
+ names -= klass.superclass.constants if klass.is_a?(Class)
196
+
197
+ names.each { |name|
198
+ const = klass.const_get(name)
199
+ next unless const.is_a?(Module)
200
+
201
+ registered << register(const, format_name("#{klass}::#{name}"), true)
202
+ register_all(const, registered)
203
+ }
204
+
205
+ registered.compact
206
+ end
207
+
208
+ protected
209
+
210
+ # call-seq:
211
+ # Athena::Formats.format_name(name) -> aString
212
+ #
213
+ # Formats +name+ as suitable format name.
214
+ def format_name(fn)
215
+ fn.sub(/\A#{self}::/, '').
216
+ gsub(/([a-z\d])(?=[A-Z])/, '\1_').
217
+ gsub(/::/, '/').downcase
52
218
  end
219
+
53
220
  end
54
221
 
55
- class Base
222
+ # Base class for all format classes. See Athena::Formats
223
+ # for more information.
56
224
 
57
- @formats = { :in => {}, :out => {} }
225
+ class Base
58
226
 
59
227
  class << self
60
228
 
61
- def formats
62
- Base.instance_variable_get(:@formats)
229
+ # call-seq:
230
+ # Athena::Formats::Base.format -> aString
231
+ #
232
+ # Returns this class's format name.
233
+ def format
234
+ @format ||= Formats.format_name(name)
63
235
  end
64
236
 
65
- def valid_format?(direction, format)
66
- if format.class < Base
67
- direction == format.class.direction
68
- else
69
- formats[direction].has_key?(format)
70
- end
237
+ # call-seq:
238
+ # Athena::Formats::Base.directions -> anArray
239
+ #
240
+ # Returns an array of the directions supported by this class.
241
+ def directions
242
+ @directions ||= []
243
+ end
244
+
245
+ # call-seq:
246
+ # Athena::Formats::Base.has_direction?(direction) -> true | false
247
+ #
248
+ # Indicates whether this class supports +direction+.
249
+ def has_direction?(direction)
250
+ directions.include?(direction)
71
251
  end
72
252
 
73
- private
253
+ # call-seq:
254
+ # Athena::Formats::Base.init(direction, *args) -> aFormat
255
+ #
256
+ # Returns a new instance of this class for +direction+ initialized
257
+ # with +args+ (see #init).
258
+ def init(direction, *args)
259
+ new.init(direction, *args)
260
+ end
74
261
 
75
- def register_format(direction, *aliases, &block)
76
- format = name.split('::').last.
77
- gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
78
- gsub(/([a-z\d])([A-Z])/, '\1_\2').
79
- downcase
262
+ protected
80
263
 
81
- register_format!(direction, format, *aliases, &block)
264
+ # call-seq:
265
+ # Athena::Formats::Base.register_format(name = nil, relax = false) -> anArray | nil
266
+ #
267
+ # Shortcut for <tt>Athena::Formats.register(self, name, relax)</tt>.
268
+ # Must be called at the end of or after the class definition (in order
269
+ # to determine the supported direction(s), the relevant instance
270
+ # methods must be available).
271
+ def register_format(name = nil, relax = false)
272
+ Formats.register(self, name, relax)
82
273
  end
83
274
 
84
- def register_format!(direction, format, *aliases, &block)
85
- raise "must be a sub-class of #{Base}" unless self < Base
275
+ end
276
+
277
+ # The _input_ format's configuration hash.
278
+ attr_reader :config
86
279
 
87
- klass = Class.new(self, &block)
280
+ # The _input_ format's "record element" (interpreted
281
+ # differently by each format).
282
+ attr_reader :record_element
88
283
 
89
- klass.instance_eval %Q{
90
- def direction; #{direction.inspect}; end
91
- def name; '#{format}::#{direction}'; end
92
- def to_s; '#{format}'; end
93
- }
284
+ # The _output_ format's output target.
285
+ attr_reader :output
94
286
 
95
- [format, *aliases].each { |name|
96
- if existing = formats[direction][name]
97
- raise DuplicateFormatDefinitionError,
98
- "format already defined (#{direction}): #{name}"
99
- else
100
- formats[direction][name] = klass
101
- end
102
- }
287
+ # call-seq:
288
+ # format.init(direction, *args) -> format
289
+ #
290
+ # Initializes _format_ for +direction+ with +args+ (see #init_in and
291
+ # #init_out), while making sure that +direction+ is actually supported
292
+ # by _format_. Returns _format_.
293
+ def init(direction, *args)
294
+ if self.class.has_direction?(direction)
295
+ send("init_#{direction}", *args)
296
+ else
297
+ raise DirectionMismatchError.new(direction, self.class.directions)
103
298
  end
104
299
 
300
+ self
105
301
  end
106
302
 
107
- def parse(*args)
303
+ # call-seq:
304
+ # format.parse(input) { |record| ... } -> anInteger
305
+ #
306
+ # Parses +input+ according to the format represented by this class
307
+ # and passes each record to the block. _Should_ return the number of
308
+ # records parsed.
309
+ #
310
+ # NOTE: Must be implemented by the sub-class in order to function as
311
+ # _input_ format.
312
+ def parse(input)
108
313
  raise NotImplementedError, 'must be defined by sub-class'
109
314
  end
110
315
 
316
+ # call-seq:
317
+ # format.convert(record) -> aString | anArray | void
318
+ #
319
+ # Converts +record+ (Athena::Record) according to the format represented
320
+ # by this class. The return value may be different for each class; it is
321
+ # irrelevant when #raw? has been defined as +true+.
322
+ #
323
+ # NOTE: Must be implemented by the sub-class in order to function as
324
+ # _output_ format.
111
325
  def convert(record)
112
326
  raise NotImplementedError, 'must be defined by sub-class'
113
327
  end
114
328
 
115
- def wrap
116
- yield self
329
+ # call-seq:
330
+ # format.raw? -> true | false
331
+ #
332
+ # Indicates whether output is written directly in #convert.
333
+ def raw?
334
+ false
117
335
  end
118
336
 
337
+ # call-seq:
338
+ # format.deferred? -> true | false
339
+ #
340
+ # Indicates whether output is to be deferred and only be written after
341
+ # all records have been converted (see #run).
119
342
  def deferred?
120
343
  false
121
344
  end
122
345
 
123
- def raw?
124
- false
346
+ # call-seq:
347
+ # format.run(spec, input) -> anInteger
348
+ #
349
+ # Runs the _output_ generation for _input_ format +spec+ (Athena::Formats::Base)
350
+ # on +input+. Outputs a sorted and unique list of records when #deferred?
351
+ # is +true+. Returns the return value of #parse.
352
+ def run(spec, input)
353
+ parsed, block = nil, if raw?
354
+ lambda { |record| record.to(self) }
355
+ elsif deferred?
356
+ deferred = []
357
+ lambda { |record| deferred << record.to(self) }
358
+ else
359
+ lambda { |record| output.puts(record.to(self)) }
360
+ end
361
+
362
+ wrap { parsed = spec.parse(input, &block) }
363
+
364
+ if deferred?
365
+ deferred.flatten!; deferred.sort!; deferred.uniq!
366
+ output.puts(deferred)
367
+ end
368
+
369
+ parsed
370
+ end
371
+
372
+ private
373
+
374
+ # call-seq:
375
+ # format.init_in(config)
376
+ #
377
+ # Initialize _input_ format (with +config+).
378
+ def init_in(config)
379
+ @config = config
380
+
381
+ case @record_element = @config.delete(:__record_element)
382
+ when *@__record_element_ok__ || String
383
+ # fine!
384
+ when nil
385
+ raise NoRecordElementError, 'no record element specified'
386
+ else
387
+ raise IllegalRecordElementError, "illegal record element #{@record_element.inspect}"
388
+ end
389
+ end
390
+
391
+ # call-seq:
392
+ # format.init_out(output = nil)
393
+ #
394
+ # Initialize _output_ format (with optional +output+).
395
+ def init_out(output = nil)
396
+ @output = output
397
+ end
398
+
399
+ # call-seq:
400
+ # format.wrap { ... }
401
+ #
402
+ # Hook for wrapping the output generation in #run.
403
+ def wrap
404
+ yield
125
405
  end
126
406
 
127
407
  end
128
408
 
129
409
  class FormatError < StandardError; end
130
410
 
131
- class DuplicateFormatDefinitionError < FormatError; end
132
- class DirectionMismatchError < FormatError; end
411
+ class DuplicateFormatDefinitionError < FormatError
412
+ def initialize(direction, format)
413
+ @direction, @format = direction, format
414
+ end
415
+
416
+ def to_s
417
+ "format already defined (#{@direction.inspect}): #{@format.inspect}"
418
+ end
419
+ end
420
+
421
+ class FormatNotFoundError < FormatError
422
+ def initialize(direction, format)
423
+ @direction, @format = direction, format
424
+ end
425
+
426
+ def to_s
427
+ "format not found (#{@direction.inspect}): #{@format.inspect}"
428
+ end
429
+ end
133
430
 
134
- ConfigError = Parser::ConfigError
431
+ class DirectionMismatchError < FormatError
432
+ def initialize(direction, directions)
433
+ @direction, @directions = direction, directions
434
+ end
435
+
436
+ def to_s
437
+ "got #{@direction.inspect}, expected one of " <<
438
+ @directions.map { |d| d.inspect }.join(', ')
439
+ end
440
+ end
441
+
442
+ class ConfigError < StandardError; end
135
443
 
136
444
  class NoRecordElementError < ConfigError; end
137
445
  class IllegalRecordElementError < ConfigError; end
@@ -143,3 +451,5 @@ end
143
451
  Dir[__FILE__.sub(/\.rb\z/, '/**/*.rb')].sort.each { |rb|
144
452
  require "athena/formats/#{File.basename(rb, '.rb')}"
145
453
  }
454
+
455
+ Athena::Formats.register_all