gotime-slither 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/History.txt +16 -0
- data/README.rdoc +109 -0
- data/Rakefile +37 -0
- data/TODO +14 -0
- data/gotime-slither.gemspec +0 -0
- data/lib/gotime-slither.rb +7 -0
- data/lib/slither/column.rb +146 -0
- data/lib/slither/definition.rb +35 -0
- data/lib/slither/generator.rb +37 -0
- data/lib/slither/parser.rb +109 -0
- data/lib/slither/section.rb +109 -0
- data/lib/slither/slither.rb +57 -0
- data/spec/column_spec.rb +229 -0
- data/spec/definition_spec.rb +86 -0
- data/spec/generator_spec.rb +48 -0
- data/spec/parser_spec.rb +297 -0
- data/spec/section_spec.rb +156 -0
- data/spec/slither_spec.rb +89 -0
- data/spec/spec_helper.rb +3 -0
- metadata +82 -0
@@ -0,0 +1,109 @@
|
|
1
|
+
class Slither
|
2
|
+
class Section
|
3
|
+
attr_accessor :definition, :optional
|
4
|
+
attr_reader :name, :columns, :options, :length
|
5
|
+
|
6
|
+
RESERVED_NAMES = [:spacer]
|
7
|
+
|
8
|
+
def initialize(name, options = {})
|
9
|
+
@name = name
|
10
|
+
@options = options
|
11
|
+
@columns = []
|
12
|
+
@trap = options[:trap]
|
13
|
+
@optional = options[:optional] || false
|
14
|
+
@length = 0
|
15
|
+
end
|
16
|
+
|
17
|
+
def column(name, length, options = {})
|
18
|
+
raise(Slither::DuplicateColumnNameError, "You have already defined a column named '#{name}'.") if @columns.map do |c|
|
19
|
+
RESERVED_NAMES.include?(c.name) ? nil : c.name
|
20
|
+
end.flatten.include?(name)
|
21
|
+
col = Column.new(name, length, @options.merge(options))
|
22
|
+
@columns << col
|
23
|
+
@length += length
|
24
|
+
col
|
25
|
+
end
|
26
|
+
|
27
|
+
def spacer(length)
|
28
|
+
column(:spacer, length)
|
29
|
+
end
|
30
|
+
|
31
|
+
def trap(&block)
|
32
|
+
@trap = block
|
33
|
+
end
|
34
|
+
|
35
|
+
def template(name)
|
36
|
+
template = @definition.templates[name]
|
37
|
+
raise ArgumentError, "Template #{name} not found as a known template." unless template
|
38
|
+
@columns += template.columns
|
39
|
+
@length += template.length
|
40
|
+
# Section options should trump template options
|
41
|
+
@options = template.options.merge(@options)
|
42
|
+
end
|
43
|
+
|
44
|
+
def format(data)
|
45
|
+
# raise( ColumnMismatchError,
|
46
|
+
# "The '#{@name}' section has #{@columns.size} column(s) defined, but there are #{data.size} column(s) provided in the data."
|
47
|
+
# ) unless @columns.size == data.size
|
48
|
+
row = ''
|
49
|
+
@columns.each do |column|
|
50
|
+
row += column.format(data[column.name])
|
51
|
+
end
|
52
|
+
row
|
53
|
+
end
|
54
|
+
|
55
|
+
def parse(line)
|
56
|
+
line_data = divide( line, @columns.map(&:length) )
|
57
|
+
row = {}
|
58
|
+
i = 0
|
59
|
+
@columns.each do |c|
|
60
|
+
unless RESERVED_NAMES.include?(c.name)
|
61
|
+
row[c.name] = (c.parse_length == 1 ?
|
62
|
+
c.parse(line_data[i]) : c.parse(line_data[i, c.parse_length]))
|
63
|
+
end
|
64
|
+
i += c.parse_length
|
65
|
+
end
|
66
|
+
row
|
67
|
+
end
|
68
|
+
|
69
|
+
def parse_when_problem(line)
|
70
|
+
line_data = divide( line, @columns.map(&:length) )
|
71
|
+
row = ''
|
72
|
+
@columns.each_with_index do |c, i|
|
73
|
+
row << "\n'#{c.name}':'#{line_data[i]}'" unless RESERVED_NAMES.include?(c.name)
|
74
|
+
end
|
75
|
+
row
|
76
|
+
end
|
77
|
+
|
78
|
+
def match(raw_line)
|
79
|
+
raw_line.nil? ? false : @trap.call(raw_line)
|
80
|
+
end
|
81
|
+
|
82
|
+
def method_missing(method, *args)
|
83
|
+
column(method, *args)
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def unpacker
|
89
|
+
@columns.map { |c| c.unpacker }.join('')
|
90
|
+
end
|
91
|
+
|
92
|
+
def divide(string, sections)
|
93
|
+
result = []
|
94
|
+
str = string.dup
|
95
|
+
unless @definition.options[:force_character_offset]
|
96
|
+
result = str.unpack(unpacker)
|
97
|
+
result.each do |s|
|
98
|
+
s.force_encoding(string.encoding) if s.respond_to? :force_encoding
|
99
|
+
end
|
100
|
+
else
|
101
|
+
sections.each do |s|
|
102
|
+
result << str.slice!(0..(s-1))
|
103
|
+
end
|
104
|
+
end
|
105
|
+
result
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
class Slither
|
2
|
+
|
3
|
+
VERSION = '0.99.5'
|
4
|
+
|
5
|
+
class DuplicateColumnNameError < StandardError; end
|
6
|
+
class RequiredSectionNotFoundError < StandardError; end
|
7
|
+
class RequiredSectionEmptyError < StandardError; end
|
8
|
+
class FormattedStringExceedsLengthError < StandardError; end
|
9
|
+
class ColumnMismatchError < StandardError; end
|
10
|
+
class LineWrongSizeError < StandardError; end
|
11
|
+
class SectionsNotSameLengthError < StandardError; end
|
12
|
+
|
13
|
+
|
14
|
+
def self.define(name, options = {}, &block)
|
15
|
+
definition = Definition.new(options)
|
16
|
+
yield(definition)
|
17
|
+
definitions[name] = definition
|
18
|
+
definition
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.generate(definition_name, data)
|
22
|
+
definition = definition(definition_name)
|
23
|
+
raise ArgumentError, "Definition name '#{name}' was not found." unless definition
|
24
|
+
generator = Generator.new(definition)
|
25
|
+
generator.generate(data)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.write(filename, definition_name, data)
|
29
|
+
File.open(filename, 'w') do |f|
|
30
|
+
f.write generate(definition_name, data)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.parse(filename, definition_name)
|
35
|
+
raise ArgumentError, "File #{filename} does not exist." unless File.exists?(filename)
|
36
|
+
|
37
|
+
file_io = File.open(filename, 'r')
|
38
|
+
parseIo(file_io, definition_name)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.parseIo(io, definition_name)
|
42
|
+
definition = definition(definition_name)
|
43
|
+
raise ArgumentError, "Definition name '#{definition_name}' was not found." unless definition
|
44
|
+
parser = Parser.new(definition, io)
|
45
|
+
definition.options[:by_bytes] ? parser.parse_by_bytes : parser.parse(definition.options[:error_handler])
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def self.definitions
|
51
|
+
@@definitions ||= {}
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.definition(name)
|
55
|
+
definitions[name]
|
56
|
+
end
|
57
|
+
end
|
data/spec/column_spec.rb
ADDED
@@ -0,0 +1,229 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
describe Slither::Column do
|
4
|
+
before(:each) do
|
5
|
+
@name = :id
|
6
|
+
@length = 5
|
7
|
+
@column = Slither::Column.new(@name, @length)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "when being created" do
|
11
|
+
it "should have a name" do
|
12
|
+
@column.name.should == @name
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should have a length" do
|
16
|
+
@column.length.should == @length
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should have a default padding" do
|
20
|
+
@column.padding.should == :space
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should have a default alignment" do
|
24
|
+
@column.alignment.should == :right
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should return a proper formatter" do
|
28
|
+
@column.send(:formatter).should == "%5s"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "when specifying an alignment" do
|
33
|
+
before(:each) do
|
34
|
+
@column = Slither::Column.new(@name, @length, :align => :left)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should only accept :right or :left for an alignment" do
|
38
|
+
lambda{ Slither::Column.new(@name, @length, :align => :bogus) }.should raise_error(ArgumentError, "Option :align only accepts :right (default) or :left")
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should override the default alignment" do
|
42
|
+
@column.alignment.should == :left
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "when specifying padding" do
|
47
|
+
before(:each) do
|
48
|
+
@column = Slither::Column.new(@name, @length, :padding => :zero)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should accept only :space or :zero" do
|
52
|
+
lambda{ Slither::Column.new(@name, @length, :padding => :bogus) }.should raise_error(ArgumentError, "Option :padding only accepts :space (default) or :zero")
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should override the default padding" do
|
56
|
+
@column.padding.should == :zero
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should return the proper unpack value for a string" do
|
61
|
+
@column.send(:unpacker).should == 'A5'
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "when parsing a value from a file" do
|
65
|
+
it "should default to a string" do
|
66
|
+
@column.parse(' name ').should == 'name'
|
67
|
+
@column.parse(' 234').should == '234'
|
68
|
+
@column.parse('000000234').should == '000000234'
|
69
|
+
@column.parse('12.34').should == '12.34'
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should support the integer type" do
|
73
|
+
@column = Slither::Column.new(:amount, 10, :type=> :integer)
|
74
|
+
@column.parse('234 ').should == 234
|
75
|
+
@column.parse(' 234').should == 234
|
76
|
+
@column.parse('00000234').should == 234
|
77
|
+
@column.parse('Ryan ').should == 0
|
78
|
+
@column.parse('00023.45').should == 23
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should accept a binary byte type" do
|
82
|
+
@column = Slither::Column.new(:dat, 1, :type=> :binary)
|
83
|
+
@column.parse(0x18).should == 0x18
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should support the float type" do
|
87
|
+
@column = Slither::Column.new(:amount, 10, :type=> :float)
|
88
|
+
@column.parse(' 234.45').should == 234.45
|
89
|
+
@column.parse('234.5600').should == 234.56
|
90
|
+
@column.parse(' 234').should == 234.0
|
91
|
+
@column.parse('00000234').should == 234.0
|
92
|
+
@column.parse('Ryan ').should == 0
|
93
|
+
@column.parse('00023.45').should == 23.45
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should support the money_with_implied_decimal type" do
|
97
|
+
@column = Slither::Column.new(:amount, 10, :type=> :money_with_implied_decimal)
|
98
|
+
@column.parse(' 23445').should == 234.45
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should support the date type" do
|
102
|
+
@column = Slither::Column.new(:date, 10, :type => :date)
|
103
|
+
dt = @column.parse('2009-08-22')
|
104
|
+
dt.should be_a(Date)
|
105
|
+
dt.to_s.should == '2009-08-22'
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should use the format option with date type if available" do
|
109
|
+
@column = Slither::Column.new(:date, 10, :type => :date, :format => "%m%d%Y")
|
110
|
+
dt = @column.parse('08222009')
|
111
|
+
dt.should be_a(Date)
|
112
|
+
dt.to_s.should == '2009-08-22'
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
describe "when applying formatting options" do
|
117
|
+
it "should return a proper formatter" do
|
118
|
+
@column = Slither::Column.new(@name, @length, :align => :left)
|
119
|
+
@column.send(:formatter).should == "%-5s"
|
120
|
+
end
|
121
|
+
|
122
|
+
it "should respect a right alignment" do
|
123
|
+
@column = Slither::Column.new(@name, @length, :align => :right)
|
124
|
+
@column.format(25).should == ' 25'
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should respect a left alignment" do
|
128
|
+
@column = Slither::Column.new(@name, @length, :align => :left)
|
129
|
+
@column.format(25).should == '25 '
|
130
|
+
end
|
131
|
+
|
132
|
+
it "should respect padding with spaces" do
|
133
|
+
@column = Slither::Column.new(@name, @length, :padding => :space)
|
134
|
+
@column.format(25).should == ' 25'
|
135
|
+
end
|
136
|
+
|
137
|
+
it "should respect padding with zeros with integer types" do
|
138
|
+
@column = Slither::Column.new(@name, @length, :type => :integer, :padding => :zero)
|
139
|
+
@column.format(25).should == '00025'
|
140
|
+
end
|
141
|
+
|
142
|
+
describe "that is a float type" do
|
143
|
+
it "should respect padding with zeros aligned right" do
|
144
|
+
@column = Slither::Column.new(@name, @length, :type => :float, :padding => :zero, :align => :right)
|
145
|
+
@column.format(4.45).should == '04.45'
|
146
|
+
end
|
147
|
+
|
148
|
+
it "should respect padding with zeros aligned left" do
|
149
|
+
@column = Slither::Column.new(@name, @length, :type => :float, :padding => :zero, :align => :left)
|
150
|
+
@column.format(4.45).should == '4.450'
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
describe "when formatting values for a file" do
|
156
|
+
it "should default to a string" do
|
157
|
+
@column = Slither::Column.new(:name, 10)
|
158
|
+
@column.format('Bill').should == ' Bill'
|
159
|
+
end
|
160
|
+
|
161
|
+
describe "whose size is too long" do
|
162
|
+
it "should raise an error if truncate is false" do
|
163
|
+
@value = "XX" * @length
|
164
|
+
lambda { @column.format(@value) }.should raise_error(
|
165
|
+
Slither::FormattedStringExceedsLengthError,
|
166
|
+
"The formatted value '#{@value}' in column '#{@name}' exceeds the allowed length of #{@length} chararacters."
|
167
|
+
)
|
168
|
+
end
|
169
|
+
|
170
|
+
it "should truncate from the left if truncate is true and aligned left" do
|
171
|
+
@column = Slither::Column.new(@name, @length, :truncate => true, :align => :left)
|
172
|
+
@column.format("This is too long").should == "This "
|
173
|
+
end
|
174
|
+
|
175
|
+
it "should truncate from the right if truncate is true and aligned right" do
|
176
|
+
@column = Slither::Column.new(@name, @length, :truncate => true, :align => :right)
|
177
|
+
@column.format("This is too long").should == " long"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
it "should support the integer type" do
|
182
|
+
@column = Slither::Column.new(:amount, 10, :type => :integer)
|
183
|
+
@column.format(234).should == ' 234'
|
184
|
+
@column.format('234').should == ' 234'
|
185
|
+
end
|
186
|
+
|
187
|
+
it "should support the float type" do
|
188
|
+
@column = Slither::Column.new(:amount, 10, :type => :float)
|
189
|
+
@column.format(234.45).should == ' 234.45'
|
190
|
+
@column.format('234.4500').should == ' 234.45'
|
191
|
+
@column.format('3').should == ' 3.0'
|
192
|
+
end
|
193
|
+
|
194
|
+
it "should support the float type with a format" do
|
195
|
+
@column = Slither::Column.new(:amount, 10, :type => :float, :format => "%.3f")
|
196
|
+
@column.format(234.45).should == ' 234.450'
|
197
|
+
@column.format('234.4500').should == ' 234.450'
|
198
|
+
@column.format('3').should == ' 3.000'
|
199
|
+
end
|
200
|
+
|
201
|
+
it "should support the float type with a format, alignment and padding" do
|
202
|
+
@column = Slither::Column.new(:amount, 10, :type => :float, :format => "%.2f", :align => :left, :padding => :zero)
|
203
|
+
@column.format(234.45).should == '234.450000'
|
204
|
+
@column = Slither::Column.new(:amount, 10, :type => :float, :format => "%.2f", :align => :right, :padding => :zero)
|
205
|
+
@column.format('234.400').should == '0000234.40'
|
206
|
+
@column = Slither::Column.new(:amount, 10, :type => :float, :format => "%.4f", :align => :left, :padding => :space)
|
207
|
+
@column.format('3').should == '3.0000 '
|
208
|
+
end
|
209
|
+
|
210
|
+
it "should support the money_with_implied_decimal type" do
|
211
|
+
@column = Slither::Column.new(:amount, 10, :type=> :money_with_implied_decimal)
|
212
|
+
@column.format(234.450).should == " 23445"
|
213
|
+
@column.format(12.34).should == " 1234"
|
214
|
+
end
|
215
|
+
|
216
|
+
it "should support the date type" do
|
217
|
+
dt = Date.new(2009, 8, 22)
|
218
|
+
@column = Slither::Column.new(:date, 10, :type => :date)
|
219
|
+
@column.format(dt).should == '2009-08-22'
|
220
|
+
end
|
221
|
+
|
222
|
+
it "should support the date type with a :format" do
|
223
|
+
dt = Date.new(2009, 8, 22)
|
224
|
+
@column = Slither::Column.new(:date, 8, :type => :date, :format => "%m%d%Y")
|
225
|
+
@column.format(dt).should == '08222009'
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
describe Slither::Definition do
|
4
|
+
before(:each) do
|
5
|
+
end
|
6
|
+
|
7
|
+
describe "when specifying alignment" do
|
8
|
+
it "should have an alignment option" do
|
9
|
+
d = Slither::Definition.new :align => :right
|
10
|
+
d.options[:align].should == :right
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should default to being right aligned" do
|
14
|
+
d = Slither::Definition.new
|
15
|
+
d.options[:align].should == :right
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should override the default if :align is passed to the section" do
|
19
|
+
d = Slither::Definition.new
|
20
|
+
d.options[:align].should == :right
|
21
|
+
d.section('name', :align => :left) {}
|
22
|
+
section = nil
|
23
|
+
d.sections.each { |sec| section = sec if sec.name == 'name' }
|
24
|
+
section.options[:align].should eq(:left)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "when creating a section" do
|
29
|
+
before(:each) do
|
30
|
+
@d = Slither::Definition.new
|
31
|
+
@section = mock('section').as_null_object
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should create and yield a new section object" do
|
35
|
+
yielded = nil
|
36
|
+
@d.section :header do |section|
|
37
|
+
yielded = section
|
38
|
+
end
|
39
|
+
yielded.should be_a(Slither::Section)
|
40
|
+
@d.sections.first.should == yielded
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should magically build a section from an unknown method" do
|
44
|
+
Slither::Section.should_receive(:new).with(:header, anything()).and_return(@section)
|
45
|
+
@d.header {}
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should not create duplicate section names" do
|
49
|
+
lambda { @d.section(:header) {} }.should_not raise_error(ArgumentError)
|
50
|
+
lambda { @d.section(:header) {} }.should raise_error(ArgumentError, "Reserved or duplicate section name: 'header'")
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should throw an error if a reserved section name is used" do
|
54
|
+
lambda { @d.section(:spacer) {} }.should raise_error(ArgumentError, "Reserved or duplicate section name: 'spacer'")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "when creating a template" do
|
59
|
+
before(:each) do
|
60
|
+
@d = Slither::Definition.new
|
61
|
+
@section = mock('section').as_null_object
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should create a new section" do
|
65
|
+
Slither::Section.should_receive(:new).with(:row, anything()).and_return(@section)
|
66
|
+
@d.template(:row) {}
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should yield the new section" do
|
70
|
+
Slither::Section.should_receive(:new).with(:row, anything()).and_return(@section)
|
71
|
+
yielded = nil
|
72
|
+
@d.template :row do |section|
|
73
|
+
yielded = section
|
74
|
+
end
|
75
|
+
yielded.should == @section
|
76
|
+
end
|
77
|
+
|
78
|
+
it "add a section to the templates collection" do
|
79
|
+
@d.should have(0).templates
|
80
|
+
@d.template :row do |t|
|
81
|
+
t.column :id, 3
|
82
|
+
end
|
83
|
+
@d.should have(1).templates
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|