athena 0.1.5 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README +1 -1
- data/Rakefile +1 -1
- data/bin/athena +2 -155
- data/lib/athena.rb +4 -7
- data/lib/athena/cli.rb +166 -0
- data/lib/athena/formats.rb +81 -75
- data/lib/athena/formats/dbm.rb +2 -3
- data/lib/athena/formats/ferret.rb +4 -4
- data/lib/athena/formats/lingo.rb +2 -3
- data/lib/athena/formats/sisis.rb +4 -4
- data/lib/athena/formats/sql.rb +8 -7
- data/lib/athena/formats/xml.rb +12 -71
- data/lib/athena/parser.rb +52 -52
- data/lib/athena/record.rb +44 -50
- data/lib/athena/version.rb +2 -2
- metadata +13 -13
- data/lib/athena/util.rb +0 -49
data/README
CHANGED
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 => [
|
17
|
+
:dependencies => %w[builder xmlstreamin] << ['ruby-nuggets', '>= 0.7.4']
|
18
18
|
}
|
19
19
|
}}
|
20
20
|
rescue LoadError => err
|
data/bin/athena
CHANGED
@@ -28,158 +28,5 @@
|
|
28
28
|
###############################################################################
|
29
29
|
#++
|
30
30
|
|
31
|
-
require '
|
32
|
-
|
33
|
-
|
34
|
-
$: << File.join(File.dirname(__FILE__), '..', 'lib')
|
35
|
-
|
36
|
-
require 'athena'
|
37
|
-
|
38
|
-
USAGE = "Usage: #{$0} [-h|--help] [options]"
|
39
|
-
abort USAGE if ARGV.empty?
|
40
|
-
|
41
|
-
# Global variable to handle verbosity
|
42
|
-
$Verbose = {}
|
43
|
-
|
44
|
-
options = {
|
45
|
-
:config => 'config.yaml',
|
46
|
-
:input => STDIN,
|
47
|
-
:output => STDOUT,
|
48
|
-
:target => nil
|
49
|
-
}
|
50
|
-
|
51
|
-
OptionParser.new { |opts|
|
52
|
-
opts.banner = USAGE
|
53
|
-
|
54
|
-
opts.separator ''
|
55
|
-
opts.separator 'Options:'
|
56
|
-
|
57
|
-
opts.on('-c', '--config YAML', "Config file [Default: #{options[:config]}#{' (currently not present)' unless File.readable?(options[:config])}]") { |f|
|
58
|
-
abort "Can't find config file: #{f}." unless File.readable?(f)
|
59
|
-
|
60
|
-
options[:config] = f
|
61
|
-
}
|
62
|
-
|
63
|
-
opts.separator ''
|
64
|
-
|
65
|
-
opts.on('-i', '--input FILE', "Input file [Default: STDIN]") { |f|
|
66
|
-
abort "Can't find input file: #{f}." unless File.readable?(f)
|
67
|
-
|
68
|
-
options[:input] = File.directory?(f) ? Dir.open(f) : File.open(f, 'r')
|
69
|
-
|
70
|
-
p = File.basename(f).split('.')
|
71
|
-
options[:spec_fallback] = p.last.downcase
|
72
|
-
options[:target_fallback] = p.size > 1 ? p[0..-2].join('.') : p.first
|
73
|
-
}
|
74
|
-
|
75
|
-
opts.on('-s', '--spec SPEC', "Input format (spec) [Default: file extension of <input-file>]") { |s|
|
76
|
-
options[:spec] = s.downcase
|
77
|
-
}
|
78
|
-
|
79
|
-
opts.on('-L', '--list-specs', "List available input formats (specs) and exit") {
|
80
|
-
puts 'Available input formats (specs):'
|
81
|
-
|
82
|
-
Athena.input_formats.each { |f, k|
|
83
|
-
puts " - #{f}#{" (= #{k})" if f != k.to_s}"
|
84
|
-
}
|
85
|
-
|
86
|
-
exit 0
|
87
|
-
}
|
88
|
-
|
89
|
-
opts.separator ''
|
90
|
-
|
91
|
-
opts.on('-o', '--output FILE', "Output file [Default: STDOUT]") { |f|
|
92
|
-
options[:output] = File.open(f, 'w')
|
93
|
-
|
94
|
-
options[:format_fallback] = f.split('.').last.downcase
|
95
|
-
}
|
96
|
-
|
97
|
-
opts.on('-f', '--format FORMAT', "Output format [Default: file extension of <output-file>]") { |f|
|
98
|
-
options[:format] = f.downcase
|
99
|
-
}
|
100
|
-
|
101
|
-
opts.on('-l', '--list-formats', "List available output formats and exit") {
|
102
|
-
puts 'Available output formats:'
|
103
|
-
|
104
|
-
Athena.output_formats.each { |f, k|
|
105
|
-
puts " - #{f}#{" (= #{k})" if f != k.to_s}"
|
106
|
-
}
|
107
|
-
|
108
|
-
exit 0
|
109
|
-
}
|
110
|
-
|
111
|
-
opts.separator ''
|
112
|
-
|
113
|
-
opts.on('-t', '--target ID', "Target whose config to use [Default: <input-file> minus file extension,", "plus '.<spec>', plus ':<format>' (reversely in turn)]") { |t|
|
114
|
-
options[:target] = t
|
115
|
-
}
|
116
|
-
|
117
|
-
opts.separator ''
|
118
|
-
opts.separator 'Generic options:'
|
119
|
-
|
120
|
-
opts.on('-v', '--verbose [WHAT]', "Be verbose about what's being done. Optional argument is a comma-separated", "list of what should be output, or 'all' [Default: 'all']") { |what|
|
121
|
-
if what.nil? || what == 'all'
|
122
|
-
$Verbose.default = true
|
123
|
-
else
|
124
|
-
what.split(',').each { |w|
|
125
|
-
$Verbose[w.to_sym] = true
|
126
|
-
}
|
127
|
-
end
|
128
|
-
}
|
129
|
-
|
130
|
-
opts.on('-h', '--help', 'Print this help message and exit') {
|
131
|
-
abort opts.to_s
|
132
|
-
}
|
133
|
-
|
134
|
-
opts.on('--version', 'Print program version and exit') {
|
135
|
-
abort "#{File.basename($0)} v#{Athena::VERSION}"
|
136
|
-
}
|
137
|
-
}.parse!
|
138
|
-
|
139
|
-
spec = options[:spec] || options[:spec_fallback]
|
140
|
-
abort "No input format (spec) specified and none could be inferred." unless spec
|
141
|
-
abort "Invalid input format (spec): #{spec}. Use '-L' to get a list of available specs." unless Athena.valid_input_format?(spec)
|
142
|
-
|
143
|
-
format = options[:format] || options[:format_fallback]
|
144
|
-
abort "No output format specified and none could be inferred." unless format
|
145
|
-
abort "Invalid output format: #{format}. Use '-l' to get a list of available formats." unless Athena.valid_output_format?(format)
|
146
|
-
|
147
|
-
yaml = YAML.load_file(options[:config])
|
148
|
-
if t = options[:target]
|
149
|
-
target = t
|
150
|
-
config = yaml[t.to_sym]
|
151
|
-
else
|
152
|
-
[options[:target_fallback] || 'generic', ".#{spec}", ":#{format}"].inject([]) { |s, t|
|
153
|
-
s << (s.last ? s.last + t : t)
|
154
|
-
}.reverse.find { |t|
|
155
|
-
target = t
|
156
|
-
config = yaml[t.to_sym]
|
157
|
-
}
|
158
|
-
end
|
159
|
-
abort "Config not found for target: #{target}." unless config
|
160
|
-
|
161
|
-
parser = Athena.parser(config, spec)
|
162
|
-
|
163
|
-
if Athena.deferred_output?(format)
|
164
|
-
res = parser.parse(options[:input])
|
165
|
-
|
166
|
-
res.map { |record|
|
167
|
-
record.to(format)
|
168
|
-
}.flatten.sort.uniq.each { |line|
|
169
|
-
options[:output].puts line
|
170
|
-
}
|
171
|
-
elsif Athena.raw_output?(format)
|
172
|
-
res = Athena.with_format(format, options[:output]) { |_format|
|
173
|
-
parser.parse(options[:input]) { |record|
|
174
|
-
record.to(_format)
|
175
|
-
}
|
176
|
-
}
|
177
|
-
else
|
178
|
-
res = Athena.with_format(format) { |_format|
|
179
|
-
parser.parse(options[:input]) { |record|
|
180
|
-
options[:output].puts record.to(_format)
|
181
|
-
}
|
182
|
-
}
|
183
|
-
end
|
184
|
-
|
185
|
-
Athena::Util.verbose(:count) { spit res.is_a?(Numeric) ? res : res.size }
|
31
|
+
require 'athena/cli'
|
32
|
+
Athena::CLI.execute
|
data/lib/athena.rb
CHANGED
@@ -35,17 +35,14 @@
|
|
35
35
|
# instance method _convert_ supplied. This way, a specific format can even function
|
36
36
|
# as both input and output format.
|
37
37
|
|
38
|
-
module Athena
|
39
|
-
end
|
40
|
-
|
41
|
-
require 'athena/util'
|
42
|
-
require 'athena/parser'
|
43
|
-
require 'athena/record'
|
44
|
-
require 'athena/formats'
|
45
38
|
require 'athena/version'
|
46
39
|
|
47
40
|
module Athena
|
48
41
|
|
42
|
+
autoload :Parser, 'athena/parser'
|
43
|
+
autoload :Record, 'athena/record'
|
44
|
+
autoload :Formats, 'athena/formats'
|
45
|
+
|
49
46
|
extend self
|
50
47
|
|
51
48
|
def parser(config, format)
|
data/lib/athena/cli.rb
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
#--
|
2
|
+
###############################################################################
|
3
|
+
# #
|
4
|
+
# A component of athena, the database file converter. #
|
5
|
+
# #
|
6
|
+
# Copyright (C) 2007-2011 University of Cologne, #
|
7
|
+
# Albertus-Magnus-Platz, #
|
8
|
+
# 50923 Cologne, Germany #
|
9
|
+
# #
|
10
|
+
# Authors: #
|
11
|
+
# Jens Wille <jens.wille@uni-koeln.de> #
|
12
|
+
# #
|
13
|
+
# athena is free software; you can redistribute it and/or modify it under the #
|
14
|
+
# terms of the GNU Affero General Public License as published by the Free #
|
15
|
+
# Software Foundation; either version 3 of the License, or (at your option) #
|
16
|
+
# any later version. #
|
17
|
+
# #
|
18
|
+
# athena is distributed in the hope that it will be useful, but WITHOUT ANY #
|
19
|
+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
|
20
|
+
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for #
|
21
|
+
# more details. #
|
22
|
+
# #
|
23
|
+
# You should have received a copy of the GNU Affero General Public License #
|
24
|
+
# along with athena. If not, see <http://www.gnu.org/licenses/>. #
|
25
|
+
# #
|
26
|
+
###############################################################################
|
27
|
+
#++
|
28
|
+
|
29
|
+
require 'nuggets/util/cli'
|
30
|
+
require 'athena'
|
31
|
+
|
32
|
+
module Athena
|
33
|
+
|
34
|
+
class CLI < ::Util::CLI
|
35
|
+
|
36
|
+
class << self
|
37
|
+
|
38
|
+
def defaults
|
39
|
+
super.merge(
|
40
|
+
:config => 'config.yaml',
|
41
|
+
:input => '-',
|
42
|
+
:output => '-',
|
43
|
+
:target => nil
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
def run(arguments)
|
50
|
+
spec = options[:spec] || options[:spec_fallback]
|
51
|
+
abort "No input format (spec) specified and none could be inferred." unless spec
|
52
|
+
abort "Invalid input format (spec): #{spec}. Use `-L' to get a list of available specs." unless Athena.valid_input_format?(spec)
|
53
|
+
|
54
|
+
format = options[:format] || options[:format_fallback]
|
55
|
+
abort "No output format specified and none could be inferred." unless format
|
56
|
+
abort "Invalid output format: #{format}. Use `-l' to get a list of available formats." unless Athena.valid_output_format?(format)
|
57
|
+
|
58
|
+
target_config = if t = options[:target]
|
59
|
+
config[target = t.to_sym]
|
60
|
+
else
|
61
|
+
[options[:target_fallback] || 'generic', ".#{spec}", ":#{format}"].inject([]) { |s, t|
|
62
|
+
s << (s.last ? s.last + t : t)
|
63
|
+
}.reverse.find { |t| config[target = t.to_sym] }
|
64
|
+
end or abort "Config not found for target: #{target}."
|
65
|
+
|
66
|
+
parser = Athena.parser(target_config, spec)
|
67
|
+
|
68
|
+
input = options[:input]
|
69
|
+
input = arguments.shift unless input != defaults[:input] || arguments.empty?
|
70
|
+
options[:input] = File.directory?(input) ? Dir.open(input) : open_file_or_std(input)
|
71
|
+
|
72
|
+
quit unless arguments.empty?
|
73
|
+
|
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
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def merge_config(args = [default])
|
102
|
+
super
|
103
|
+
end
|
104
|
+
|
105
|
+
def opts(opts)
|
106
|
+
opts.on('-c', '--config YAML', "Config file [Default: #{defaults[:config]}#{' (currently not present)' unless File.readable?(defaults[:config])}]") { |config|
|
107
|
+
quit "Can't find config file: #{config}" unless File.readable?(config)
|
108
|
+
|
109
|
+
options[:config] = config
|
110
|
+
}
|
111
|
+
|
112
|
+
opts.separator ''
|
113
|
+
|
114
|
+
opts.on('-i', '--input FILE', "Input file [Default: STDIN]") { |input|
|
115
|
+
options[:input] = input
|
116
|
+
|
117
|
+
parts = File.basename(input).split('.')
|
118
|
+
options[:spec_fallback] = parts.last.downcase
|
119
|
+
options[:target_fallback] = parts.size > 1 ? parts[0..-2].join('.') : parts.first
|
120
|
+
}
|
121
|
+
|
122
|
+
opts.on('-s', '--spec SPEC', "Input format (spec) [Default: file extension of <input-file>]") { |spec|
|
123
|
+
options[:spec] = spec.downcase
|
124
|
+
}
|
125
|
+
|
126
|
+
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
|
134
|
+
}
|
135
|
+
|
136
|
+
opts.separator ''
|
137
|
+
|
138
|
+
opts.on('-o', '--output FILE', "Output file [Default: STDOUT]") { |output|
|
139
|
+
options[:output] = output
|
140
|
+
options[:format_fallback] = output.split('.').last.downcase
|
141
|
+
}
|
142
|
+
|
143
|
+
opts.on('-f', '--format FORMAT', "Output format [Default: file extension of <output-file>]") { |format|
|
144
|
+
options[:format] = format.downcase
|
145
|
+
}
|
146
|
+
|
147
|
+
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
|
155
|
+
}
|
156
|
+
|
157
|
+
opts.separator ''
|
158
|
+
|
159
|
+
opts.on('-t', '--target ID', "Target whose config to use [Default: <input-file> minus file extension,", "plus '.<spec>', plus ':<format>' (reversely in turn)]") { |target|
|
160
|
+
options[:target] = target
|
161
|
+
}
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
data/lib/athena/formats.rb
CHANGED
@@ -26,114 +26,120 @@
|
|
26
26
|
###############################################################################
|
27
27
|
#++
|
28
28
|
|
29
|
+
require 'athena'
|
30
|
+
|
29
31
|
module Athena
|
32
|
+
|
30
33
|
module Formats
|
31
34
|
|
32
|
-
|
33
|
-
|
35
|
+
CRLF = %Q{\015\012}
|
36
|
+
CRLF_RE = %r{(?:\r?\n)+}
|
34
37
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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}"
|
44
|
+
else
|
45
|
+
format
|
46
|
+
end
|
41
47
|
else
|
42
|
-
format
|
48
|
+
Base.formats[direction][format].new
|
43
49
|
end
|
44
50
|
else
|
45
|
-
Base.formats[direction][format]
|
51
|
+
Base.formats[direction][format]
|
46
52
|
end
|
47
|
-
else
|
48
|
-
Base.formats[direction][format]
|
49
53
|
end
|
50
|
-
end
|
51
|
-
|
52
|
-
class Base
|
53
54
|
|
54
|
-
|
55
|
+
class Base
|
55
56
|
|
56
|
-
|
57
|
+
@formats = { :in => {}, :out => {} }
|
57
58
|
|
58
|
-
|
59
|
-
Base.instance_variable_get(:@formats)
|
60
|
-
end
|
59
|
+
class << self
|
61
60
|
|
62
|
-
|
63
|
-
|
64
|
-
direction == format.class.direction
|
65
|
-
else
|
66
|
-
formats[direction].has_key?(format)
|
61
|
+
def formats
|
62
|
+
Base.instance_variable_get(:@formats)
|
67
63
|
end
|
68
|
-
end
|
69
64
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
71
|
+
end
|
77
72
|
|
78
|
-
|
79
|
-
end
|
73
|
+
private
|
80
74
|
|
81
|
-
|
82
|
-
|
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
|
83
80
|
|
84
|
-
|
81
|
+
register_format!(direction, format, *aliases, &block)
|
82
|
+
end
|
85
83
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
84
|
+
def register_format!(direction, format, *aliases, &block)
|
85
|
+
raise "must be a sub-class of #{Base}" unless self < Base
|
86
|
+
|
87
|
+
klass = Class.new(self, &block)
|
88
|
+
|
89
|
+
klass.instance_eval %Q{
|
90
|
+
def direction; #{direction.inspect}; end
|
91
|
+
def name; '#{format}::#{direction}'; end
|
92
|
+
def to_s; '#{format}'; end
|
93
|
+
}
|
94
|
+
|
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
|
+
}
|
103
|
+
end
|
91
104
|
|
92
|
-
[format, *aliases].each { |name|
|
93
|
-
if existing = formats[direction][name]
|
94
|
-
raise DuplicateFormatDefinitionError,
|
95
|
-
"format already defined (#{direction}): #{name}"
|
96
|
-
else
|
97
|
-
formats[direction][name] = klass
|
98
|
-
end
|
99
|
-
}
|
100
105
|
end
|
101
106
|
|
102
|
-
|
107
|
+
def parse(*args)
|
108
|
+
raise NotImplementedError, 'must be defined by sub-class'
|
109
|
+
end
|
103
110
|
|
104
|
-
|
105
|
-
|
106
|
-
|
111
|
+
def convert(record)
|
112
|
+
raise NotImplementedError, 'must be defined by sub-class'
|
113
|
+
end
|
107
114
|
|
108
|
-
|
109
|
-
|
110
|
-
|
115
|
+
def wrap
|
116
|
+
yield self
|
117
|
+
end
|
111
118
|
|
112
|
-
|
113
|
-
|
114
|
-
|
119
|
+
def deferred?
|
120
|
+
false
|
121
|
+
end
|
115
122
|
|
116
|
-
|
117
|
-
|
118
|
-
|
123
|
+
def raw?
|
124
|
+
false
|
125
|
+
end
|
119
126
|
|
120
|
-
def raw?
|
121
|
-
false
|
122
127
|
end
|
123
128
|
|
124
|
-
|
125
|
-
|
126
|
-
class FormatError < StandardError; end
|
129
|
+
class FormatError < StandardError; end
|
127
130
|
|
128
|
-
|
129
|
-
|
131
|
+
class DuplicateFormatDefinitionError < FormatError; end
|
132
|
+
class DirectionMismatchError < FormatError; end
|
130
133
|
|
131
|
-
|
134
|
+
ConfigError = Parser::ConfigError
|
132
135
|
|
133
|
-
|
134
|
-
|
136
|
+
class NoRecordElementError < ConfigError; end
|
137
|
+
class IllegalRecordElementError < ConfigError; end
|
135
138
|
|
136
139
|
end
|
140
|
+
|
137
141
|
end
|
138
142
|
|
139
|
-
Dir[__FILE__.sub(/\.rb\z/, '/**/*.rb')].sort.each { |rb|
|
143
|
+
Dir[__FILE__.sub(/\.rb\z/, '/**/*.rb')].sort.each { |rb|
|
144
|
+
require "athena/formats/#{File.basename(rb, '.rb')}"
|
145
|
+
}
|
data/lib/athena/formats/dbm.rb
CHANGED
@@ -26,6 +26,8 @@
|
|
26
26
|
###############################################################################
|
27
27
|
#++
|
28
28
|
|
29
|
+
require 'athena'
|
30
|
+
|
29
31
|
if ferret_version = ENV['FERRET_VERSION']
|
30
32
|
require 'rubygems'
|
31
33
|
gem 'ferret', ferret_version
|
@@ -37,8 +39,7 @@ rescue LoadError => err
|
|
37
39
|
warn "ferret#{" #{ferret_version}" if ferret_version} not available (#{err})"
|
38
40
|
end
|
39
41
|
|
40
|
-
module Athena
|
41
|
-
module Formats
|
42
|
+
module Athena::Formats
|
42
43
|
|
43
44
|
class Ferret < Base
|
44
45
|
|
@@ -88,7 +89,7 @@ module Athena
|
|
88
89
|
unless index.deleted?(i)
|
89
90
|
doc = index[i]
|
90
91
|
|
91
|
-
Record.new(doc[record_element], block) { |record|
|
92
|
+
Athena::Record.new(doc[record_element], block) { |record|
|
92
93
|
config.each { |element, field_config|
|
93
94
|
record.update(element, doc[element], field_config)
|
94
95
|
}
|
@@ -101,5 +102,4 @@ module Athena
|
|
101
102
|
|
102
103
|
end
|
103
104
|
|
104
|
-
end
|
105
105
|
end
|
data/lib/athena/formats/lingo.rb
CHANGED
data/lib/athena/formats/sisis.rb
CHANGED
@@ -26,8 +26,9 @@
|
|
26
26
|
###############################################################################
|
27
27
|
#++
|
28
28
|
|
29
|
-
|
30
|
-
|
29
|
+
require 'athena'
|
30
|
+
|
31
|
+
module Athena::Formats
|
31
32
|
|
32
33
|
class Sisis < Base
|
33
34
|
|
@@ -60,7 +61,7 @@ module Athena
|
|
60
61
|
|
61
62
|
if element == record_element
|
62
63
|
record.close if record
|
63
|
-
record = Record.new(value, block)
|
64
|
+
record = Athena::Record.new(value, block)
|
64
65
|
num += 1
|
65
66
|
else
|
66
67
|
record.update(element, value, config[element])
|
@@ -74,5 +75,4 @@ module Athena
|
|
74
75
|
|
75
76
|
end
|
76
77
|
|
77
|
-
end
|
78
78
|
end
|
data/lib/athena/formats/sql.rb
CHANGED
@@ -27,9 +27,9 @@
|
|
27
27
|
#++
|
28
28
|
|
29
29
|
require 'strscan'
|
30
|
+
require 'athena'
|
30
31
|
|
31
|
-
module Athena
|
32
|
-
module Formats
|
32
|
+
module Athena::Formats
|
33
33
|
|
34
34
|
class MYSQL < Base
|
35
35
|
|
@@ -68,7 +68,7 @@ module Athena
|
|
68
68
|
next if _columns.empty?
|
69
69
|
|
70
70
|
sql_parser.parse($2) { |row|
|
71
|
-
Record.new(nil, block) { |record|
|
71
|
+
Athena::Record.new(nil, block) { |record|
|
72
72
|
row.each_with_index { |value, index|
|
73
73
|
column = _columns[index] or next
|
74
74
|
|
@@ -158,7 +158,9 @@ module Athena
|
|
158
158
|
end
|
159
159
|
|
160
160
|
def parse_string_escape
|
161
|
-
if @input.scan(/\\[
|
161
|
+
if @input.scan(/\\[abtnvfr]/)
|
162
|
+
AST.new(eval(%Q{"#{@input.matched}"}))
|
163
|
+
elsif @input.scan(/\\.|''/)
|
162
164
|
AST.new(@input.matched[-1, 1])
|
163
165
|
end
|
164
166
|
end
|
@@ -179,7 +181,7 @@ module Athena
|
|
179
181
|
if @input.eos?
|
180
182
|
raise "Unexpected end of input (#{message})."
|
181
183
|
else
|
182
|
-
raise "#{message} at #{@input.pos}: #{@input.peek(
|
184
|
+
raise "#{message} at #{$.}:#{@input.pos}: #{@input.peek(16).inspect}"
|
183
185
|
end
|
184
186
|
end
|
185
187
|
|
@@ -221,7 +223,7 @@ module Athena
|
|
221
223
|
cols = columns[table]
|
222
224
|
next if cols.empty?
|
223
225
|
|
224
|
-
Record.new(nil, block) { |record|
|
226
|
+
Athena::Record.new(nil, block) { |record|
|
225
227
|
line.split(/\t/).each_with_index { |value, index|
|
226
228
|
column = cols[index] or next
|
227
229
|
|
@@ -245,5 +247,4 @@ module Athena
|
|
245
247
|
MySQL = MYSQL
|
246
248
|
PgSQL = PGSQL
|
247
249
|
|
248
|
-
end
|
249
250
|
end
|
data/lib/athena/formats/xml.rb
CHANGED
@@ -27,18 +27,15 @@
|
|
27
27
|
#++
|
28
28
|
|
29
29
|
require 'forwardable'
|
30
|
-
|
31
30
|
require 'builder'
|
32
31
|
require 'xmlstreamin'
|
33
32
|
require 'nuggets/hash/insert'
|
33
|
+
require 'athena'
|
34
34
|
|
35
|
-
module Athena
|
36
|
-
module Formats
|
35
|
+
module Athena::Formats
|
37
36
|
|
38
37
|
class XML < Base
|
39
38
|
|
40
|
-
include Util
|
41
|
-
|
42
39
|
# <http://www.w3.org/TR/2006/REC-xml-20060816/#NT-Name>
|
43
40
|
ELEMENT_START_RE = %r{\A[a-zA-Z_:]}
|
44
41
|
NON_ELEMENT_CHAR_RE = %r{[^\w:.-]}
|
@@ -104,11 +101,7 @@ module Athena
|
|
104
101
|
|
105
102
|
def wrap(out = nil)
|
106
103
|
res = nil
|
107
|
-
|
108
|
-
builder(:target => out).database {
|
109
|
-
res = super()
|
110
|
-
}
|
111
|
-
|
104
|
+
builder(:target => out).database { res = super() }
|
112
105
|
res
|
113
106
|
end
|
114
107
|
|
@@ -184,89 +177,44 @@ module Athena
|
|
184
177
|
spec.default!(prev_spec)
|
185
178
|
}
|
186
179
|
|
187
|
-
verbose(:spec, BaseSpec) { spec.inspect_spec }
|
188
|
-
|
189
180
|
XMLStreamin::XMLStreamListener.new(spec)
|
190
181
|
end
|
191
182
|
|
192
183
|
def define_spec(element, field, config, arg)
|
193
184
|
spec = ElementSpec.new(element, field, config)
|
194
|
-
|
195
|
-
case arg
|
196
|
-
when Hash then spec.specs!(arg)
|
197
|
-
else spec.default!(SubElementSpec.new(spec))
|
198
|
-
end
|
199
|
-
|
185
|
+
arg.is_a?(Hash) ? spec.specs!(arg) : spec.default!(SubElementSpec.new(spec))
|
200
186
|
spec
|
201
187
|
end
|
202
188
|
|
203
189
|
def merge_specs(container, key, spec)
|
204
|
-
container.insert!(key => spec) { |_,
|
205
|
-
if
|
206
|
-
|
207
|
-
|
190
|
+
container.insert!(key => spec) { |_, spec1, spec2|
|
191
|
+
if spec1.respond_to?(:specs!)
|
192
|
+
spec1.specs!(spec2.respond_to?(:specs) ? spec2.specs : spec2)
|
193
|
+
spec1
|
208
194
|
else
|
209
|
-
|
195
|
+
spec1.merge(spec2)
|
210
196
|
end
|
211
197
|
}
|
212
198
|
end
|
213
199
|
|
214
200
|
class BaseSpec < XMLStreamin::XMLSpec
|
215
201
|
|
216
|
-
include Util
|
217
|
-
|
218
|
-
@level = 0
|
219
|
-
|
220
202
|
def start(context, name, attrs)
|
221
|
-
verbose(:xml) {
|
222
|
-
spit "#{indent(level)}<#{name}>"; step :down
|
223
|
-
attrs.each { |attr| spit "#{indent(level + 1)}[#{attr[0]} = #{attr[1]}]" }
|
224
|
-
}
|
225
|
-
|
226
203
|
context
|
227
204
|
end
|
228
205
|
|
229
206
|
def text(context, data)
|
230
|
-
verbose(:xml) { spit "#{indent(level)}#{data.strip}" unless data.strip.empty? }
|
231
207
|
context
|
232
208
|
end
|
233
209
|
|
234
210
|
def done(context, name)
|
235
|
-
verbose(:xml) { step :up; spit "#{indent(level)}</#{name}>" }
|
236
211
|
context
|
237
212
|
end
|
238
213
|
|
239
214
|
def empty(context)
|
240
|
-
verbose(:xml) { step :up }
|
241
215
|
context
|
242
216
|
end
|
243
217
|
|
244
|
-
def inspect_spec(element = nil, level = 0)
|
245
|
-
if respond_to?(:field)
|
246
|
-
msg = "#{indent(level)}[#{element}] #{field.to_s.upcase} -> #{name}"
|
247
|
-
respond_to?(:spit) ? spit(msg) : warn(msg)
|
248
|
-
|
249
|
-
inspect_specs(level + 1)
|
250
|
-
else
|
251
|
-
specs.empty? ? specs.default.inspect_spec('?', level) : inspect_specs(level)
|
252
|
-
end
|
253
|
-
end
|
254
|
-
|
255
|
-
private
|
256
|
-
|
257
|
-
def inspect_specs(level = 0)
|
258
|
-
specs.each { |element, spec| spec.inspect_spec(element, level) }
|
259
|
-
end
|
260
|
-
|
261
|
-
def level
|
262
|
-
BaseSpec.instance_variable_get(:@level)
|
263
|
-
end
|
264
|
-
|
265
|
-
def step(direction)
|
266
|
-
steps = { :down => 1, :up => -1 }
|
267
|
-
BaseSpec.instance_variable_set(:@level, level + steps[direction])
|
268
|
-
end
|
269
|
-
|
270
218
|
end
|
271
219
|
|
272
220
|
class RecordSpec < BaseSpec
|
@@ -276,13 +224,12 @@ module Athena
|
|
276
224
|
|
277
225
|
def initialize(&block)
|
278
226
|
super()
|
279
|
-
|
280
227
|
@block = block
|
281
228
|
end
|
282
229
|
|
283
230
|
def start(context, name, attrs)
|
284
231
|
context = super
|
285
|
-
self.record = Record.new(nil, block, true)
|
232
|
+
self.record = Athena::Record.new(nil, block, true)
|
286
233
|
context
|
287
234
|
end
|
288
235
|
|
@@ -301,15 +248,12 @@ module Athena
|
|
301
248
|
|
302
249
|
def initialize(name, field, config)
|
303
250
|
super()
|
304
|
-
|
305
|
-
@name = name
|
306
|
-
@field = field
|
307
|
-
@config = config
|
251
|
+
@name, @field, @config = name, field, config
|
308
252
|
end
|
309
253
|
|
310
254
|
def start(context, name, attrs)
|
311
255
|
context = super
|
312
|
-
self.record = Record[field, config]
|
256
|
+
self.record = Athena::Record[field, config]
|
313
257
|
context
|
314
258
|
end
|
315
259
|
|
@@ -330,9 +274,7 @@ module Athena
|
|
330
274
|
|
331
275
|
def initialize(parent)
|
332
276
|
super()
|
333
|
-
|
334
277
|
@parent = parent
|
335
|
-
|
336
278
|
default!(self)
|
337
279
|
end
|
338
280
|
|
@@ -340,5 +282,4 @@ module Athena
|
|
340
282
|
|
341
283
|
end
|
342
284
|
|
343
|
-
end
|
344
285
|
end
|
data/lib/athena/parser.rb
CHANGED
@@ -26,65 +26,65 @@
|
|
26
26
|
###############################################################################
|
27
27
|
#++
|
28
28
|
|
29
|
-
|
30
|
-
class Parser
|
31
|
-
|
32
|
-
include Util
|
33
|
-
|
34
|
-
DEFAULT_SEPARATOR = ', '
|
35
|
-
DEFAULT_EMPTY = '<<EMPTY>>'
|
36
|
-
|
37
|
-
attr_reader :config, :spec
|
38
|
-
|
39
|
-
def initialize(config, spec)
|
40
|
-
@config = build_config(config)
|
41
|
-
@spec = Formats[:in, spec].new(self)
|
42
|
-
end
|
43
|
-
|
44
|
-
def parse(source, &block)
|
45
|
-
res = spec.parse(source, &block)
|
46
|
-
res.is_a?(Numeric) ? res : Record.records
|
47
|
-
end
|
29
|
+
require 'athena'
|
48
30
|
|
49
|
-
|
50
|
-
|
51
|
-
def build_config(config)
|
52
|
-
hash = {}
|
31
|
+
module Athena
|
53
32
|
|
54
|
-
|
55
|
-
if field.to_s =~ /\A__/
|
56
|
-
hash[field] = value
|
57
|
-
else
|
58
|
-
case value
|
59
|
-
when String, Array
|
60
|
-
elements, value = [*value], {}
|
61
|
-
when Hash
|
62
|
-
elements = value[:elements] || value[:element].to_a
|
33
|
+
class Parser
|
63
34
|
|
64
|
-
|
65
|
-
|
66
|
-
|
35
|
+
DEFAULT_SEPARATOR = ', '
|
36
|
+
DEFAULT_EMPTY = '<<EMPTY>>'
|
37
|
+
|
38
|
+
attr_reader :config, :spec
|
39
|
+
|
40
|
+
def initialize(config, spec)
|
41
|
+
@config = build_config(config)
|
42
|
+
@spec = Formats[:in, spec].new(self)
|
43
|
+
end
|
44
|
+
|
45
|
+
def parse(source, &block)
|
46
|
+
res = spec.parse(source, &block)
|
47
|
+
res.is_a?(Numeric) ? res : Record.records
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def build_config(config)
|
53
|
+
hash = {}
|
54
|
+
|
55
|
+
config.each { |field, value|
|
56
|
+
if field.to_s =~ /\A__/
|
57
|
+
hash[field] = value
|
58
|
+
else
|
59
|
+
case value
|
60
|
+
when String, Array
|
61
|
+
elements, value = [*value], {}
|
62
|
+
when Hash
|
63
|
+
elements = value[:elements] || value[:element].to_a
|
64
|
+
|
65
|
+
raise ArgumentError, "no elements specified for field #{field}" unless elements.is_a?(Array)
|
66
|
+
else
|
67
|
+
raise ArgumentError, "illegal value for field #{field}"
|
68
|
+
end
|
69
|
+
|
70
|
+
separator = value[:separator] || DEFAULT_SEPARATOR
|
71
|
+
|
72
|
+
elements.each { |element|
|
73
|
+
(hash[element] ||= {})[field] = {
|
74
|
+
:string => value[:string] || ['%s'] * elements.size * separator,
|
75
|
+
:empty => value[:empty] || DEFAULT_EMPTY,
|
76
|
+
:elements => elements
|
77
|
+
}
|
78
|
+
}
|
67
79
|
end
|
80
|
+
}
|
68
81
|
|
69
|
-
|
82
|
+
hash
|
83
|
+
end
|
70
84
|
|
71
|
-
|
72
|
-
|
85
|
+
class ConfigError < StandardError
|
86
|
+
end
|
73
87
|
|
74
|
-
(hash[element] ||= {})[field] = {
|
75
|
-
:string => value[:string] || ['%s'] * elements.size * separator,
|
76
|
-
:empty => value[:empty] || DEFAULT_EMPTY,
|
77
|
-
:elements => elements
|
78
|
-
}
|
79
|
-
}
|
80
|
-
end
|
81
|
-
}
|
82
|
-
|
83
|
-
hash
|
84
|
-
end
|
85
|
-
|
86
|
-
class ConfigError < StandardError
|
87
88
|
end
|
88
89
|
|
89
|
-
end
|
90
90
|
end
|
data/lib/athena/record.rb
CHANGED
@@ -27,80 +27,74 @@
|
|
27
27
|
#++
|
28
28
|
|
29
29
|
require 'nuggets/integer/map'
|
30
|
+
require 'athena'
|
30
31
|
|
31
32
|
module Athena
|
33
|
+
|
32
34
|
class Record
|
33
35
|
|
34
|
-
|
36
|
+
@records = []
|
35
37
|
|
36
|
-
|
38
|
+
class << self
|
37
39
|
|
38
|
-
|
40
|
+
def records
|
41
|
+
@records
|
42
|
+
end
|
39
43
|
|
40
|
-
|
41
|
-
|
42
|
-
|
44
|
+
def [](field = nil, config = nil)
|
45
|
+
record = records.last
|
46
|
+
raise NoRecordError unless record
|
43
47
|
|
44
|
-
|
45
|
-
|
46
|
-
|
48
|
+
record.fill(field, config) if field && config
|
49
|
+
record
|
50
|
+
end
|
47
51
|
|
48
|
-
record.fill(field, config) if field && config
|
49
|
-
record
|
50
52
|
end
|
51
53
|
|
52
|
-
|
53
|
-
|
54
|
-
attr_reader :struct, :block, :id
|
54
|
+
attr_reader :struct, :block, :id
|
55
55
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
56
|
+
def initialize(id = nil, block = nil, add = !block)
|
57
|
+
@id = id || object_id.map_positive
|
58
|
+
@block = block
|
59
|
+
@struct = {}
|
60
60
|
|
61
|
-
|
61
|
+
add_record if add
|
62
62
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
63
|
+
if block_given?
|
64
|
+
begin
|
65
|
+
yield self
|
66
|
+
ensure
|
67
|
+
close
|
68
|
+
end
|
68
69
|
end
|
69
70
|
end
|
70
|
-
end
|
71
71
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
def update(element, data, field_config = nil)
|
77
|
-
field_config.each { |field, config| fill(field, config) } if field_config
|
72
|
+
def fill(field, config)
|
73
|
+
struct[field] ||= config.merge(:values => Hash.new { |h, k| h[k] = [] })
|
74
|
+
end
|
78
75
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
76
|
+
def update(element, data, field_config = nil)
|
77
|
+
field_config.each { |field, config| fill(field, config) } if field_config
|
78
|
+
struct.each_key { |field| struct[field][:values][element] << data }
|
79
|
+
end
|
83
80
|
|
84
|
-
|
85
|
-
|
86
|
-
|
81
|
+
def close
|
82
|
+
block ? block[self] : self
|
83
|
+
end
|
87
84
|
|
88
|
-
|
89
|
-
|
90
|
-
|
85
|
+
def to(format)
|
86
|
+
Formats[:out, format].convert(self)
|
87
|
+
end
|
91
88
|
|
92
|
-
|
93
|
-
Formats[:out, format].convert(self)
|
94
|
-
end
|
89
|
+
private
|
95
90
|
|
96
|
-
|
91
|
+
def add_record
|
92
|
+
self.class.records << self
|
93
|
+
end
|
97
94
|
|
98
|
-
|
99
|
-
|
100
|
-
end
|
95
|
+
class NoRecordError < StandardError
|
96
|
+
end
|
101
97
|
|
102
|
-
class NoRecordError < StandardError
|
103
98
|
end
|
104
99
|
|
105
|
-
end
|
106
100
|
end
|
data/lib/athena/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: athena
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 21
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
+
- 2
|
8
9
|
- 1
|
9
|
-
|
10
|
-
version: 0.1.5
|
10
|
+
version: 0.2.1
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Jens Wille
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-07-
|
18
|
+
date: 2011-07-27 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: builder
|
@@ -53,12 +53,12 @@ dependencies:
|
|
53
53
|
requirements:
|
54
54
|
- - ">="
|
55
55
|
- !ruby/object:Gem::Version
|
56
|
-
hash:
|
56
|
+
hash: 11
|
57
57
|
segments:
|
58
58
|
- 0
|
59
|
-
-
|
59
|
+
- 7
|
60
60
|
- 4
|
61
|
-
version: 0.
|
61
|
+
version: 0.7.4
|
62
62
|
type: :runtime
|
63
63
|
version_requirements: *id003
|
64
64
|
description: Convert database files to various formats.
|
@@ -72,8 +72,8 @@ extra_rdoc_files:
|
|
72
72
|
- COPYING
|
73
73
|
- ChangeLog
|
74
74
|
files:
|
75
|
-
- lib/athena/util.rb
|
76
75
|
- lib/athena/record.rb
|
76
|
+
- lib/athena/cli.rb
|
77
77
|
- lib/athena/formats/xml.rb
|
78
78
|
- lib/athena/formats/lingo.rb
|
79
79
|
- lib/athena/formats/ferret.rb
|
@@ -99,14 +99,14 @@ licenses: []
|
|
99
99
|
|
100
100
|
post_install_message:
|
101
101
|
rdoc_options:
|
102
|
+
- --all
|
103
|
+
- --main
|
104
|
+
- README
|
102
105
|
- --charset
|
103
106
|
- UTF-8
|
104
107
|
- --title
|
105
|
-
- athena Application documentation (v0.1
|
106
|
-
- --main
|
107
|
-
- README
|
108
|
+
- athena Application documentation (v0.2.1)
|
108
109
|
- --line-numbers
|
109
|
-
- --all
|
110
110
|
require_paths:
|
111
111
|
- lib
|
112
112
|
required_ruby_version: !ruby/object:Gem::Requirement
|
@@ -130,7 +130,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
130
130
|
requirements: []
|
131
131
|
|
132
132
|
rubyforge_project: prometheus
|
133
|
-
rubygems_version: 1.8.
|
133
|
+
rubygems_version: 1.8.6
|
134
134
|
signing_key:
|
135
135
|
specification_version: 3
|
136
136
|
summary: Convert database files to various formats.
|
data/lib/athena/util.rb
DELETED
@@ -1,49 +0,0 @@
|
|
1
|
-
#--
|
2
|
-
###############################################################################
|
3
|
-
# #
|
4
|
-
# A component of athena, the database file converter. #
|
5
|
-
# #
|
6
|
-
# Copyright (C) 2007-2011 University of Cologne, #
|
7
|
-
# Albertus-Magnus-Platz, #
|
8
|
-
# 50923 Cologne, Germany #
|
9
|
-
# #
|
10
|
-
# Authors: #
|
11
|
-
# Jens Wille <jens.wille@uni-koeln.de> #
|
12
|
-
# #
|
13
|
-
# athena is free software; you can redistribute it and/or modify it under the #
|
14
|
-
# terms of the GNU Affero General Public License as published by the Free #
|
15
|
-
# Software Foundation; either version 3 of the License, or (at your option) #
|
16
|
-
# any later version. #
|
17
|
-
# #
|
18
|
-
# athena is distributed in the hope that it will be useful, but WITHOUT ANY #
|
19
|
-
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
|
20
|
-
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for #
|
21
|
-
# more details. #
|
22
|
-
# #
|
23
|
-
# You should have received a copy of the GNU Affero General Public License #
|
24
|
-
# along with athena. If not, see <http://www.gnu.org/licenses/>. #
|
25
|
-
# #
|
26
|
-
###############################################################################
|
27
|
-
#++
|
28
|
-
|
29
|
-
module Athena
|
30
|
-
module Util
|
31
|
-
|
32
|
-
extend self
|
33
|
-
|
34
|
-
def verbose(what, klass = self.class, &block)
|
35
|
-
if $Verbose && $Verbose[what]
|
36
|
-
klass.send(:define_method, :spit) { |msg|
|
37
|
-
warn "*#{what}: #{msg}"
|
38
|
-
}
|
39
|
-
|
40
|
-
klass.send(:define_method, :indent) { |*level|
|
41
|
-
' ' * (level.first || 0)
|
42
|
-
}
|
43
|
-
|
44
|
-
instance_eval(&block)
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
end
|
49
|
-
end
|