ryanwood-slither 0.99.0 → 0.99.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +7 -1
- data/README.rdoc +5 -13
- data/TODO +12 -3
- data/lib/slither/column.rb +53 -17
- data/lib/slither/definition.rb +3 -3
- data/lib/slither/generator.rb +2 -5
- data/lib/slither/parser.rb +1 -4
- data/lib/slither/section.rb +8 -1
- data/lib/slither/slither.rb +6 -0
- data/slither.gemspec +0 -0
- data/spec/column_spec.rb +74 -14
- data/spec/generator_spec.rb +1 -1
- data/spec/parser_spec.rb +1 -1
- data/spec/section_spec.rb +36 -18
- metadata +1 -1
data/History.txt
CHANGED
@@ -1,4 +1,10 @@
|
|
1
1
|
== 0.99.0 / 2009-04-14
|
2
2
|
|
3
3
|
* Initial Release
|
4
|
-
|
4
|
+
|
5
|
+
== 0.99.1 / 2009-04-22
|
6
|
+
|
7
|
+
* Make the missing method build a column i.e. body.record_type 1
|
8
|
+
* Prevent duplicate column names
|
9
|
+
* Better error messages
|
10
|
+
* Implement custom padding (spaces (default), zero)
|
data/README.rdoc
CHANGED
@@ -24,31 +24,23 @@ A simple, clean DSL for describing, writing, and parsing fixed-width text files.
|
|
24
24
|
end
|
25
25
|
|
26
26
|
# Create a header section
|
27
|
-
d.header, :align => :left do |header|
|
28
|
-
|
27
|
+
d.header, :align => :left do |header|
|
29
28
|
# The trap tells Slither which lines should fall into this section
|
30
|
-
header.trap
|
31
|
-
line[0,4] == 'HEAD'
|
32
|
-
end
|
33
|
-
|
29
|
+
header.trap { |line| line[0,4] == 'HEAD' }
|
34
30
|
# Use the boundary template for the columns
|
35
31
|
header.template :boundary
|
36
32
|
end
|
37
33
|
|
38
34
|
d.body do |body|
|
39
|
-
body.trap
|
40
|
-
line[0,4] =~ /[^(HEAD|FOOT)]/
|
41
|
-
end
|
35
|
+
body.trap { |line| line[0,4] =~ /[^(HEAD|FOOT)]/ }
|
42
36
|
body.column :id, 10, :type => :integer
|
43
37
|
body.column :name, 10, :align => :left
|
44
38
|
body.spacer 3
|
45
39
|
body.column :state, 2
|
46
40
|
end
|
47
41
|
|
48
|
-
d.footer
|
49
|
-
footer.trap
|
50
|
-
line[0,4] == 'FOOT'
|
51
|
-
end
|
42
|
+
d.footer do |footer|
|
43
|
+
footer.trap { |line| line[0,4] == 'FOOT' }
|
52
44
|
footer.template :boundary
|
53
45
|
footer.column :record_count, 10
|
54
46
|
end
|
data/TODO
CHANGED
@@ -1,4 +1,13 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
== 0.99.2
|
2
|
+
|
3
|
+
* Add :limit option on sections
|
4
|
+
* Add :validation option for columns
|
5
|
+
* Add a validate_file() method to parse a file and run all validation tests (implies validation implemented)
|
6
|
+
|
7
|
+
== 1.0.0
|
8
|
+
|
3
9
|
* Better Documentation
|
4
|
-
|
10
|
+
|
11
|
+
== 1.x
|
12
|
+
|
13
|
+
* Alternate Section Flow (other than linear), i.e. repeatable sections (think batch)
|
data/lib/slither/column.rb
CHANGED
@@ -2,7 +2,7 @@ require 'date'
|
|
2
2
|
|
3
3
|
class Slither
|
4
4
|
class Column
|
5
|
-
attr_reader :name, :length, :alignment, :type, :options
|
5
|
+
attr_reader :name, :length, :alignment, :type, :padding, :precision, :options
|
6
6
|
|
7
7
|
def initialize(name, length, options = {})
|
8
8
|
assert_valid_options(options)
|
@@ -11,16 +11,15 @@ class Slither
|
|
11
11
|
@options = options
|
12
12
|
@alignment = options[:align] || :right
|
13
13
|
@type = options[:type] || :string
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
"%#{aligner}#{length}s"
|
14
|
+
@padding = options[:padding] || :space
|
15
|
+
# Only used with floats, this determines the decimal places
|
16
|
+
@precision = options[:precision]
|
18
17
|
end
|
19
18
|
|
20
19
|
def unpacker
|
21
20
|
"A#{@length}"
|
22
21
|
end
|
23
|
-
|
22
|
+
|
24
23
|
def to_type(value)
|
25
24
|
case @type
|
26
25
|
when :integer: value.to_i
|
@@ -35,28 +34,65 @@ class Slither
|
|
35
34
|
end
|
36
35
|
end
|
37
36
|
|
38
|
-
def
|
39
|
-
|
40
|
-
when :date:
|
41
|
-
if @options[:date_format]
|
42
|
-
value.strftime(@options[:date_format])
|
43
|
-
else
|
44
|
-
value.strftime
|
45
|
-
end
|
46
|
-
else value.to_s
|
47
|
-
end
|
37
|
+
def format(value)
|
38
|
+
pad(formatter % format_as_string(value))
|
48
39
|
end
|
49
|
-
|
40
|
+
|
50
41
|
private
|
51
42
|
|
43
|
+
def formatter
|
44
|
+
"%#{aligner}#{sizer}#{typer}"
|
45
|
+
end
|
46
|
+
|
52
47
|
def aligner
|
53
48
|
@alignment == :left ? '-' : ''
|
54
49
|
end
|
55
50
|
|
51
|
+
def sizer
|
52
|
+
(@type == :float && @precision) ? @precision : @length
|
53
|
+
end
|
54
|
+
|
55
|
+
def typer
|
56
|
+
case @type
|
57
|
+
when :integer: 'd'
|
58
|
+
when :float: 's'
|
59
|
+
else 's'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Manually apply padding. sprintf only allows padding on numeric fields.
|
64
|
+
def pad(value)
|
65
|
+
return value unless @padding == :zero
|
66
|
+
matcher = @alignment == :right ? /^ +/ : / +$/
|
67
|
+
space = value.match(matcher)
|
68
|
+
return value unless space
|
69
|
+
value.gsub(space[0], '0' * space[0].size)
|
70
|
+
end
|
71
|
+
|
72
|
+
def format_as_string(value)
|
73
|
+
result = case @type
|
74
|
+
when :date:
|
75
|
+
if @options[:date_format]
|
76
|
+
value.strftime(@options[:date_format])
|
77
|
+
else
|
78
|
+
value.strftime
|
79
|
+
end
|
80
|
+
else value.to_s
|
81
|
+
end
|
82
|
+
raise(
|
83
|
+
Slither::FormattedStringExceedsLengthError,
|
84
|
+
"The formatted value '#{result}' exceeds #{@length} chararacters, the allowable length of the '#{@name}' column."
|
85
|
+
) if result.length > @length
|
86
|
+
result
|
87
|
+
end
|
88
|
+
|
56
89
|
def assert_valid_options(options)
|
57
90
|
unless options[:align].nil? || [:left, :right].include?(options[:align])
|
58
91
|
raise ArgumentError, "Option :align only accepts :right (default) or :left"
|
59
92
|
end
|
93
|
+
unless options[:padding].nil? || [:space, :zero].include?(options[:padding])
|
94
|
+
raise ArgumentError, "Option :padding only accepts :space (default) or :zero"
|
95
|
+
end
|
60
96
|
end
|
61
97
|
end
|
62
98
|
end
|
data/lib/slither/definition.rb
CHANGED
@@ -10,8 +10,8 @@ class Slither
|
|
10
10
|
|
11
11
|
def section(name, options = {}, &block)
|
12
12
|
raise( ArgumentError, "Reserved or duplicate section name: '#{name}'") if
|
13
|
-
|
14
|
-
|
13
|
+
Section::RESERVED_NAMES.include?( name ) ||
|
14
|
+
(@sections.size > 0 && @sections.map{ |s| s.name }.include?( name ))
|
15
15
|
|
16
16
|
section = Slither::Section.new(name, @options.merge(options))
|
17
17
|
section.definition = self
|
@@ -27,7 +27,7 @@ class Slither
|
|
27
27
|
end
|
28
28
|
|
29
29
|
def method_missing(method, *args, &block)
|
30
|
-
|
30
|
+
section(method, *args, &block)
|
31
31
|
end
|
32
32
|
end
|
33
33
|
end
|
data/lib/slither/generator.rb
CHANGED
@@ -1,7 +1,4 @@
|
|
1
1
|
class Slither
|
2
|
-
|
3
|
-
class RequiredSectionEmptyError < StandardError; end
|
4
|
-
|
5
2
|
class Generator
|
6
3
|
|
7
4
|
def initialize(definition)
|
@@ -14,12 +11,12 @@ class Slither
|
|
14
11
|
content = data[section.name]
|
15
12
|
if content
|
16
13
|
content = [content] unless content.is_a?(Array)
|
17
|
-
raise
|
14
|
+
raise(Slither::RequiredSectionEmptyError, "Required section '#{section.name}' was empty.") if content.empty?
|
18
15
|
content.each do |row|
|
19
16
|
@builder << section.format(row)
|
20
17
|
end
|
21
18
|
else
|
22
|
-
raise
|
19
|
+
raise(Slither::RequiredSectionEmptyError, "Required section '#{section.name}' was empty.") unless section.optional
|
23
20
|
end
|
24
21
|
end
|
25
22
|
@builder.join("\n")
|
data/lib/slither/parser.rb
CHANGED
@@ -1,7 +1,4 @@
|
|
1
1
|
class Slither
|
2
|
-
|
3
|
-
class RequiredSectionNotFoundError < StandardError; end
|
4
|
-
|
5
2
|
class Parser
|
6
3
|
|
7
4
|
def initialize(definition, file)
|
@@ -17,7 +14,7 @@ class Slither
|
|
17
14
|
unless @content.empty?
|
18
15
|
@definition.sections.each do |section|
|
19
16
|
rows = fill_content(section)
|
20
|
-
raise
|
17
|
+
raise(Slither::RequiredSectionNotFoundError, "Required section '#{section.name}' was not found.") unless rows > 0 || section.optional
|
21
18
|
end
|
22
19
|
end
|
23
20
|
@parsed
|
data/lib/slither/section.rb
CHANGED
@@ -14,6 +14,9 @@ class Slither
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def column(name, length, options = {})
|
17
|
+
raise(Slither::DuplicateColumnNameError, "You have already defined a column named '#{name}'.") if @columns.map do |c|
|
18
|
+
RESERVED_NAMES.include?(c.name) ? nil : c.name
|
19
|
+
end.flatten.include?(name)
|
17
20
|
col = Column.new(name, length, @options.merge(options))
|
18
21
|
@columns << col
|
19
22
|
col
|
@@ -38,7 +41,7 @@ class Slither
|
|
38
41
|
def format(data)
|
39
42
|
row = ''
|
40
43
|
@columns.each do |column|
|
41
|
-
row +=
|
44
|
+
row += column.format(data[column.name])
|
42
45
|
end
|
43
46
|
row
|
44
47
|
end
|
@@ -55,6 +58,10 @@ class Slither
|
|
55
58
|
def match(raw_line)
|
56
59
|
raw_line.nil? ? false : @trap.call(raw_line)
|
57
60
|
end
|
61
|
+
|
62
|
+
def method_missing(method, *args)
|
63
|
+
column(method, *args)
|
64
|
+
end
|
58
65
|
|
59
66
|
private
|
60
67
|
|
data/lib/slither/slither.rb
CHANGED
@@ -2,6 +2,12 @@ class Slither
|
|
2
2
|
|
3
3
|
VERSION = '0.99.0'
|
4
4
|
|
5
|
+
class DuplicateColumnNameError < StandardError; end
|
6
|
+
class RequiredSectionNotFoundError < StandardError; end
|
7
|
+
class RequiredSectionEmptyError < StandardError; end
|
8
|
+
class FormattedStringExceedsLengthError < StandardError; end
|
9
|
+
|
10
|
+
|
5
11
|
def self.define(name, options = {}, &block)
|
6
12
|
definition = Definition.new(options)
|
7
13
|
yield(definition)
|
data/slither.gemspec
CHANGED
Binary file
|
data/spec/column_spec.rb
CHANGED
@@ -16,12 +16,16 @@ describe Slither::Column do
|
|
16
16
|
@column.length.should == @length
|
17
17
|
end
|
18
18
|
|
19
|
+
it "should have a default padding" do
|
20
|
+
@column.padding.should == :space
|
21
|
+
end
|
22
|
+
|
19
23
|
it "should have a default alignment" do
|
20
24
|
@column.alignment.should == :right
|
21
25
|
end
|
22
26
|
|
23
|
-
|
24
|
-
@column.formatter.should == "%5s"
|
27
|
+
it "should return a proper formatter" do
|
28
|
+
@column.send(:formatter).should == "%5s"
|
25
29
|
end
|
26
30
|
end
|
27
31
|
|
@@ -37,14 +41,24 @@ describe Slither::Column do
|
|
37
41
|
it "should override the default alignment" do
|
38
42
|
@column.alignment.should == :left
|
39
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
|
40
50
|
|
41
|
-
it "should
|
42
|
-
@
|
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
|
43
57
|
end
|
44
58
|
end
|
45
59
|
|
46
60
|
it "should return the proper unpack value for a string" do
|
47
|
-
@column.unpacker.should == 'A5'
|
61
|
+
@column.send(:unpacker).should == 'A5'
|
48
62
|
end
|
49
63
|
|
50
64
|
describe "when typing the value" do
|
@@ -77,31 +91,77 @@ describe Slither::Column do
|
|
77
91
|
end
|
78
92
|
end
|
79
93
|
|
80
|
-
describe "when
|
94
|
+
describe "when getting the column's the value as a string" do
|
81
95
|
it "should default to a string" do
|
82
|
-
@column.
|
96
|
+
@column.send(:format_as_string, 'name').should == 'name'
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should raise an error if the value is longer than the length" do
|
100
|
+
lambda { @column.send(:format_as_string, 'This string is too long') }.should raise_error(
|
101
|
+
Slither::FormattedStringExceedsLengthError,
|
102
|
+
"The formatted value 'This string is too long' exceeds #{@length} chararacters, the allowable length of the '#{@name}' column."
|
103
|
+
)
|
83
104
|
end
|
84
105
|
|
85
106
|
it "should support the :integer type" do
|
86
107
|
@column = Slither::Column.new(@name, @length, :type => :integer)
|
87
|
-
@column.
|
108
|
+
@column.send(:format_as_string, 234).should == '234'
|
88
109
|
end
|
89
110
|
|
90
111
|
it "should support the :float type" do
|
91
|
-
@column = Slither::Column.new(
|
92
|
-
@column.
|
112
|
+
@column = Slither::Column.new(:amount, 6, :type => :float)
|
113
|
+
@column.send(:format_as_string, 234.45).should == '234.45'
|
93
114
|
end
|
94
115
|
|
95
116
|
it "should support the :date type" do
|
96
117
|
dt = Date.new(2009, 8, 22)
|
97
|
-
@column = Slither::Column.new(
|
98
|
-
@column.
|
118
|
+
@column = Slither::Column.new(:date, 10, :type => :date)
|
119
|
+
@column.send(:format_as_string, dt).should == '2009-08-22'
|
99
120
|
end
|
100
121
|
|
101
122
|
it "should use the :date_format option with :date type if available" do
|
102
123
|
dt = Date.new(2009, 8, 22)
|
103
|
-
@column = Slither::Column.new(
|
104
|
-
@column.
|
124
|
+
@column = Slither::Column.new(:date, 8, :type => :date, :date_format => "%m%d%Y")
|
125
|
+
@column.send(:format_as_string, dt).should == '08222009'
|
105
126
|
end
|
106
127
|
end
|
128
|
+
|
129
|
+
describe "when formatting a column" do
|
130
|
+
it "should return a proper formatter" do
|
131
|
+
@column = Slither::Column.new(@name, @length, :align => :left)
|
132
|
+
@column.send(:formatter).should == "%-5s"
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should respect a right alignment" do
|
136
|
+
@column = Slither::Column.new(@name, @length, :align => :right)
|
137
|
+
@column.format(25).should == ' 25'
|
138
|
+
end
|
139
|
+
|
140
|
+
it "should respect a left alignment" do
|
141
|
+
@column = Slither::Column.new(@name, @length, :align => :left)
|
142
|
+
@column.format(25).should == '25 '
|
143
|
+
end
|
144
|
+
|
145
|
+
it "should respect padding with spaces" do
|
146
|
+
@column = Slither::Column.new(@name, @length, :padding => :space)
|
147
|
+
@column.format(25).should == ' 25'
|
148
|
+
end
|
149
|
+
|
150
|
+
it "should respect padding with zeros with integer types" do
|
151
|
+
@column = Slither::Column.new(@name, @length, :type => :integer, :padding => :zero)
|
152
|
+
@column.format(25).should == '00025'
|
153
|
+
end
|
154
|
+
|
155
|
+
describe "that is a float type" do
|
156
|
+
it "should respect padding with zeros aligned right" do
|
157
|
+
@column = Slither::Column.new(@name, @length, :type => :float, :padding => :zero, :align => :right)
|
158
|
+
@column.format(4.45).should == '04.45'
|
159
|
+
end
|
160
|
+
|
161
|
+
it "should respect padding with zeros aligned left" do
|
162
|
+
@column = Slither::Column.new(@name, @length, :type => :float, :padding => :zero, :align => :left)
|
163
|
+
@column.format(4.45).should == '4.450'
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
107
167
|
end
|
data/spec/generator_spec.rb
CHANGED
@@ -32,7 +32,7 @@ describe Slither::Generator do
|
|
32
32
|
|
33
33
|
it "should raise an error if there is no data for a required section" do
|
34
34
|
@data.delete :header
|
35
|
-
lambda { @generator.generate(@data) }.should raise_error(Slither::RequiredSectionEmptyError)
|
35
|
+
lambda { @generator.generate(@data) }.should raise_error(Slither::RequiredSectionEmptyError, "Required section 'header' was empty.")
|
36
36
|
end
|
37
37
|
|
38
38
|
it "should generate a string" do
|
data/spec/parser_spec.rb
CHANGED
@@ -66,7 +66,7 @@ describe Slither::Parser do
|
|
66
66
|
|
67
67
|
it "should raise an error if a required section is not found" do
|
68
68
|
@file.should_receive(:gets).twice.and_return(' Ryan Wood', nil)
|
69
|
-
lambda { @parser.parse }.should raise_error(Slither::RequiredSectionNotFoundError)
|
69
|
+
lambda { @parser.parse }.should raise_error(Slither::RequiredSectionNotFoundError, "Required section 'header' was not found.")
|
70
70
|
end
|
71
71
|
|
72
72
|
it "raise an error if a section limit is over run"
|
data/spec/section_spec.rb
CHANGED
@@ -13,28 +13,46 @@ describe Slither::Section do
|
|
13
13
|
Slither::Section::RESERVED_NAMES.should == [:spacer]
|
14
14
|
end
|
15
15
|
|
16
|
-
|
17
|
-
|
16
|
+
describe "when adding columns" do
|
17
|
+
it "should build an ordered column list" do
|
18
|
+
@section.should have(0).columns
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
20
|
+
col1 = @section.column :id, 10
|
21
|
+
col2 = @section.column :name, 30
|
22
|
+
col3 = @section.column :state, 2
|
22
23
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
24
|
+
@section.should have(3).columns
|
25
|
+
@section.columns[0].should be(col1)
|
26
|
+
@section.columns[1].should be(col2)
|
27
|
+
@section.columns[2].should be(col3)
|
28
|
+
end
|
28
29
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
30
|
+
it "should create spacer columns" do
|
31
|
+
@section.should have(0).columns
|
32
|
+
@section.spacer(5)
|
33
|
+
@section.should have(1).columns
|
34
|
+
end
|
34
35
|
|
35
|
-
|
36
|
-
|
37
|
-
|
36
|
+
it "can should override the alignment of the definition" do
|
37
|
+
section = Slither::Section.new('name', :align => :left)
|
38
|
+
section.options[:align].should == :left
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should use a missing method to create a column" do
|
42
|
+
@section.should have(0).columns
|
43
|
+
@section.first_name 5
|
44
|
+
@section.should have(1).columns
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should prevent duplicate column names" do
|
48
|
+
@section.column :id, 10
|
49
|
+
lambda { @section.column(:id, 30) }.should raise_error(Slither::DuplicateColumnNameError, "You have already defined a column named 'id'.")
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should allow duplicate column names that are reserved (i.e. spacer)" do
|
53
|
+
@section.spacer 10
|
54
|
+
lambda { @section.spacer 10 }.should_not raise_error(Slither::DuplicateColumnNameError)
|
55
|
+
end
|
38
56
|
end
|
39
57
|
|
40
58
|
it "should accept and store the trap as a block" do
|