slither-ruby19 0.99.3

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,16 @@
1
+
2
+ == 0.99.2 / 2009-04-28
3
+
4
+ * Added better support for float formatting
5
+ * Added the money_with_implied_decimal type
6
+
7
+ == 0.99.1 / 2009-04-22
8
+
9
+ * Make the missing method build a column i.e. body.record_type 1
10
+ * Prevent duplicate column names
11
+ * Better error messages
12
+ * Implement custom padding (spaces (default), zero)
13
+
14
+ == 0.99.0 / 2009-04-14
15
+
16
+ * Initial Release
data/README.rdoc ADDED
@@ -0,0 +1,100 @@
1
+ == slither
2
+ by Ryan Wood
3
+ http://ryanwood.com
4
+
5
+ == DESCRIPTION:
6
+
7
+ A simple, clean DSL for describing, writing, and parsing fixed-width text files.
8
+
9
+ == FEATURES:
10
+
11
+ * Easy DSL syntax
12
+ * Can parse and format fixed width files
13
+ * Templated sections for reuse
14
+
15
+ == SYNOPSIS:
16
+
17
+ # Create a Slither::Defintion to describe a file format
18
+ Slither.define :simple do |d|
19
+
20
+ # This is a template section that can be reused in other sections
21
+ d.template :boundary do |t|
22
+ t.column :record_type, 4
23
+ t.column :company_id, 12
24
+ end
25
+
26
+ # Create a header section
27
+ d.header, :align => :left do |header|
28
+ # The trap tells Slither which lines should fall into this section
29
+ header.trap { |line| line[0,4] == 'HEAD' }
30
+ # Use the boundary template for the columns
31
+ header.template :boundary
32
+ end
33
+
34
+ d.body do |body|
35
+ body.trap { |line| line[0,4] =~ /[^(HEAD|FOOT)]/ }
36
+ body.column :id, 10, :type => :integer
37
+ body.column :name, 10, :align => :left
38
+ body.spacer 3
39
+ body.column :state, 2
40
+ end
41
+
42
+ d.footer do |footer|
43
+ footer.trap { |line| line[0,4] == 'FOOT' }
44
+ footer.template :boundary
45
+ footer.column :record_count, 10
46
+ end
47
+ end
48
+
49
+ Supported types are: string, integer, date, float, money, and money_with_implied_decimal.
50
+
51
+ Then either feed it a nested struct with data values to create the file in the defined format:
52
+
53
+ test_data = {
54
+ :body => [
55
+ { :id => 12, :name => "Ryan", :state => 'SC' },
56
+ { :id => 23, :name => "Joe", :state => 'VA' },
57
+ { :id => 42, :name => "Tommy", :state => 'FL' },
58
+ ],
59
+ :header => { :record_type => 'HEAD', :company_id => 'ABC' },
60
+ :footer => { :record_type => 'FOOT', :company_id => 'ABC' }
61
+ }
62
+
63
+ # Generates the file as a string
64
+ puts Slither.generate(:simple, test_data)
65
+
66
+ # Writes the file
67
+ Slither.write('outfile.txt', :simple, test_data)
68
+
69
+ or parse files already in that format into a nested hash:
70
+
71
+ parsed_data = Slither.parse('infile.txt', :test).inspect
72
+
73
+ == INSTALL:
74
+
75
+ sudo gem install slither
76
+
77
+ == LICENSE:
78
+
79
+ (The MIT License)
80
+
81
+ Copyright (c) 2008
82
+
83
+ Permission is hereby granted, free of charge, to any person obtaining
84
+ a copy of this software and associated documentation files (the
85
+ 'Software'), to deal in the Software without restriction, including
86
+ without limitation the rights to use, copy, modify, merge, publish,
87
+ distribute, sublicense, and/or sell copies of the Software, and to
88
+ permit persons to whom the Software is furnished to do so, subject to
89
+ the following conditions:
90
+
91
+ The above copyright notice and this permission notice shall be
92
+ included in all copies or substantial portions of the Software.
93
+
94
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
95
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
96
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
97
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
98
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
99
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
100
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,37 @@
1
+ require 'rake'
2
+ require 'spec/rake/spectask'
3
+
4
+ desc "Run all examples with RCov"
5
+ Spec::Rake::SpecTask.new('rcov') do |t|
6
+ t.spec_files = FileList['spec/*.rb']
7
+ t.rcov = true
8
+ t.rcov_opts = ['--exclude', 'spec']
9
+ end
10
+
11
+ begin
12
+ require 'bones'
13
+ Bones.setup
14
+ rescue LoadError
15
+ load 'tasks/setup.rb'
16
+ end
17
+
18
+ ensure_in_path 'lib'
19
+ require 'bones'
20
+
21
+ task :default => 'spec:run'
22
+
23
+ PROJ.name = 'slither'
24
+ PROJ.authors = 'Ryan Wood'
25
+ PROJ.email = 'ryan.wood@gmail.com'
26
+ PROJ.url = 'http://github.com/ryanwood/slither'
27
+ PROJ.version = '0.99.3'
28
+ PROJ.exclude = %w(\.git .gitignore ^tasks \.eprj ^pkg)
29
+ PROJ.readme_file = 'README.rdoc'
30
+
31
+ #PROJ.rubyforge.name = 'codeforpeople'
32
+
33
+ PROJ.rdoc.exclude << '^data'
34
+ PROJ.notes.exclude = %w(^README\.rdoc$ ^data ^pkg)
35
+
36
+ # PROJ.svn.path = 'bones'
37
+ # PROJ.spec.opts << '--color'
data/TODO ADDED
@@ -0,0 +1,13 @@
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
+
9
+ * Better Documentation
10
+
11
+ == 1.x
12
+
13
+ * Alternate Section Flow (other than linear), i.e. repeatable sections (think batch)
data/lib/slither.rb ADDED
@@ -0,0 +1,7 @@
1
+ $: << File.dirname(__FILE__)
2
+ require 'slither/slither'
3
+ require 'slither/definition'
4
+ require 'slither/section'
5
+ require 'slither/column'
6
+ require 'slither/parser'
7
+ require 'slither/generator'
@@ -0,0 +1,126 @@
1
+ require 'date'
2
+
3
+ class Slither
4
+ class ParserError < RuntimeError; end
5
+
6
+ class Column
7
+ attr_reader :name, :length, :alignment, :type, :padding, :precision, :options
8
+
9
+ def initialize(name, length, options = {})
10
+ assert_valid_options(options)
11
+ @name = name
12
+ @length = length
13
+ @options = options
14
+ @alignment = options[:align] || :right
15
+ @type = options[:type] || :string
16
+ @padding = options[:padding] || :space
17
+ @truncate = options[:truncate] || false
18
+ # Only used with floats, this determines the decimal places
19
+ @precision = options[:precision]
20
+ end
21
+
22
+ def unpacker
23
+ "A#{@length}"
24
+ end
25
+
26
+ def parse(value)
27
+ case @type
28
+ when :integer
29
+ value.to_i
30
+ when :float, :money
31
+ value.to_f
32
+ when :money_with_implied_decimal
33
+ value.to_f / 100
34
+ when :date
35
+ if @options[:format]
36
+ Date.strptime(value, @options[:format])
37
+ else
38
+ Date.strptime(value)
39
+ end
40
+ else value.strip
41
+ end
42
+ rescue
43
+ raise ParserError, "The value '#{value}' could not be converted to type #{@type}: #{$!}"
44
+ end
45
+
46
+ def format(value)
47
+ pad(formatter % to_s(value))
48
+ rescue
49
+ puts "Could not format column '#{@name}' as a '#{@type}' with formatter '#{formatter}' and value of '#{value}' (formatted: '#{to_s(value)}'). #{$!}"
50
+ end
51
+
52
+ private
53
+
54
+ def formatter
55
+ "%#{aligner}#{sizer}s"
56
+ end
57
+
58
+ def aligner
59
+ @alignment == :left ? '-' : ''
60
+ end
61
+
62
+ def sizer
63
+ (@type == :float && @precision) ? @precision : @length
64
+ end
65
+
66
+ # Manually apply padding. sprintf only allows padding on numeric fields.
67
+ def pad(value)
68
+ return value unless @padding == :zero
69
+ matcher = @alignment == :right ? /^ +/ : / +$/
70
+ space = value.match(matcher)
71
+ return value unless space
72
+ value.gsub(space[0], '0' * space[0].size)
73
+ end
74
+
75
+ def to_s(value)
76
+ result = case @type
77
+ when :date
78
+ # If it's a DBI::Timestamp object, see if we can convert it to a Time object
79
+ unless value.respond_to?(:strftime)
80
+ value = value.to_time if value.respond_to?(:to_time)
81
+ end
82
+ if value.respond_to?(:strftime)
83
+ if @options[:format]
84
+ value.strftime(@options[:format])
85
+ else
86
+ value.strftime
87
+ end
88
+ else
89
+ value.to_s
90
+ end
91
+ when :float
92
+ @options[:format] ? @options[:format] % value.to_f : value.to_f.to_s
93
+ when :money
94
+ "%.2f" % value.to_f
95
+ when :money_with_implied_decimal
96
+ "%d" % (value.to_f * 100)
97
+ else
98
+ value.to_s
99
+ end
100
+ validate_size result
101
+ end
102
+
103
+ def assert_valid_options(options)
104
+ unless options[:align].nil? || [:left, :right].include?(options[:align])
105
+ raise ArgumentError, "Option :align only accepts :right (default) or :left"
106
+ end
107
+ unless options[:padding].nil? || [:space, :zero].include?(options[:padding])
108
+ raise ArgumentError, "Option :padding only accepts :space (default) or :zero"
109
+ end
110
+ end
111
+
112
+ def validate_size(result)
113
+ # Handle when length is out of range
114
+ if result.length > @length
115
+ if @truncate
116
+ start = @alignment == :left ? 0 : -@length
117
+ result = result[start, @length]
118
+ else
119
+ raise Slither::FormattedStringExceedsLengthError,
120
+ "The formatted value '#{result}' in column '#{@name}' exceeds the allowed length of #{@length} chararacters."
121
+ end
122
+ end
123
+ result
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,33 @@
1
+ class Slither
2
+ class Definition
3
+ attr_reader :sections, :templates, :options
4
+
5
+ def initialize(options = {})
6
+ @sections = []
7
+ @templates = {}
8
+ @options = { :align => :right }.merge(options)
9
+ end
10
+
11
+ def section(name, options = {}, &block)
12
+ raise( ArgumentError, "Reserved or duplicate section name: '#{name}'") if
13
+ Section::RESERVED_NAMES.include?( name ) ||
14
+ (@sections.size > 0 && @sections.map{ |s| s.name }.include?( name ))
15
+
16
+ section = Slither::Section.new(name, @options.merge(options))
17
+ section.definition = self
18
+ yield(section)
19
+ @sections << section
20
+ section
21
+ end
22
+
23
+ def template(name, options = {}, &block)
24
+ section = Slither::Section.new(name, @options.merge(options))
25
+ yield(section)
26
+ @templates[name] = section
27
+ end
28
+
29
+ def method_missing(method, *args, &block)
30
+ section(method, *args, &block)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,26 @@
1
+ class Slither
2
+ class Generator
3
+
4
+ def initialize(definition)
5
+ @definition = definition
6
+ end
7
+
8
+ def generate(data)
9
+ @builder = []
10
+ @definition.sections.each do |section|
11
+ content = data[section.name]
12
+ if content
13
+ content = [content] unless content.is_a?(Array)
14
+ raise(Slither::RequiredSectionEmptyError, "Required section '#{section.name}' was empty.") if content.empty?
15
+ content.each do |row|
16
+ @builder << section.format(row)
17
+ end
18
+ else
19
+ raise(Slither::RequiredSectionEmptyError, "Required section '#{section.name}' was empty.") unless section.optional
20
+ end
21
+ end
22
+ @builder.join("\n")
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,53 @@
1
+ class Slither
2
+ class Parser
3
+
4
+ def initialize(definition, file)
5
+ @definition = definition
6
+ @file = file
7
+ # This may be used in the future for non-linear or repeating sections
8
+ @mode = :linear
9
+ end
10
+
11
+ def parse()
12
+ @parsed = {}
13
+ @content = read_file
14
+ unless @content.empty?
15
+ @definition.sections.each do |section|
16
+ rows = fill_content(section)
17
+ raise(Slither::RequiredSectionNotFoundError, "Required section '#{section.name}' was not found.") unless rows > 0 || section.optional
18
+ end
19
+ end
20
+ @parsed
21
+ end
22
+
23
+ private
24
+
25
+ def read_file
26
+ content = []
27
+ File.open(@file, 'r') do |f|
28
+ while (line = f.gets) do
29
+ content << line
30
+ end
31
+ end
32
+ content
33
+ end
34
+
35
+ def fill_content(section)
36
+ matches = 0
37
+ loop do
38
+ line = @content.first
39
+ break unless section.match(line)
40
+ add_to_section(section, line)
41
+ matches += 1
42
+ @content.shift
43
+ end
44
+ matches
45
+ end
46
+
47
+ def add_to_section(section, line)
48
+ @parsed[section.name] = [] unless @parsed[section.name]
49
+ @parsed[section.name] << section.parse(line)
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,76 @@
1
+ class Slither
2
+ class Section
3
+ attr_accessor :definition, :optional
4
+ attr_reader :name, :columns, :options
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
+ end
15
+
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)
20
+ col = Column.new(name, length, @options.merge(options))
21
+ @columns << col
22
+ col
23
+ end
24
+
25
+ def spacer(length)
26
+ column(:spacer, length)
27
+ end
28
+
29
+ def trap(&block)
30
+ @trap = block
31
+ end
32
+
33
+ def template(name)
34
+ template = @definition.templates[name]
35
+ raise ArgumentError, "Template #{name} not found as a known template." unless template
36
+ @columns = @columns + template.columns
37
+ # Section options should trump template options
38
+ @options = template.options.merge(@options)
39
+ end
40
+
41
+ def format(data)
42
+ # raise( ColumnMismatchError,
43
+ # "The '#{@name}' section has #{@columns.size} column(s) defined, but there are #{data.size} column(s) provided in the data."
44
+ # ) unless @columns.size == data.size
45
+ row = ''
46
+ @columns.each do |column|
47
+ row += column.format(data[column.name])
48
+ end
49
+ row
50
+ end
51
+
52
+ def parse(line)
53
+ line_data = line.unpack(unpacker)
54
+ row = {}
55
+ @columns.each_with_index do |c, i|
56
+ row[c.name] = c.parse(line_data[i]) unless RESERVED_NAMES.include?(c.name)
57
+ end
58
+ row
59
+ end
60
+
61
+ def match(raw_line)
62
+ raw_line.nil? ? false : @trap.call(raw_line)
63
+ end
64
+
65
+ def method_missing(method, *args)
66
+ column(method, *args)
67
+ end
68
+
69
+ private
70
+
71
+ def unpacker
72
+ @columns.map { |c| c.unpacker }.join('')
73
+ end
74
+
75
+ end
76
+ end
@@ -0,0 +1,49 @@
1
+ class Slither
2
+
3
+ VERSION = '0.99.0'
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
+
11
+
12
+ def self.define(name, options = {}, &block)
13
+ definition = Definition.new(options)
14
+ yield(definition)
15
+ definitions[name] = definition
16
+ definition
17
+ end
18
+
19
+ def self.generate(definition_name, data)
20
+ definition = definition(definition_name)
21
+ raise ArgumentError, "Definition name '#{name}' was not found." unless definition
22
+ generator = Generator.new(definition)
23
+ generator.generate(data)
24
+ end
25
+
26
+ def self.write(filename, definition_name, data)
27
+ File.open(filename, 'w') do |f|
28
+ f.write generate(definition_name, data)
29
+ end
30
+ end
31
+
32
+ def self.parse(filename, definition_name)
33
+ raise ArgumentError, "File #{filename} does not exist." unless File.exists?(filename)
34
+ definition = definition(definition_name)
35
+ raise ArgumentError, "Definition name '#{definition_name}' was not found." unless definition
36
+ parser = Parser.new(definition, filename)
37
+ parser.parse
38
+ end
39
+
40
+ private
41
+
42
+ def self.definitions
43
+ @@definitions ||= {}
44
+ end
45
+
46
+ def self.definition(name)
47
+ definitions[name]
48
+ end
49
+ end
data/slither.gemspec ADDED
Binary file
@@ -0,0 +1,224 @@
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 support the float type" do
82
+ @column = Slither::Column.new(:amount, 10, :type=> :float)
83
+ @column.parse(' 234.45').should == 234.45
84
+ @column.parse('234.5600').should == 234.56
85
+ @column.parse(' 234').should == 234.0
86
+ @column.parse('00000234').should == 234.0
87
+ @column.parse('Ryan ').should == 0
88
+ @column.parse('00023.45').should == 23.45
89
+ end
90
+
91
+ it "should support the money_with_implied_decimal type" do
92
+ @column = Slither::Column.new(:amount, 10, :type=> :money_with_implied_decimal)
93
+ @column.parse(' 23445').should == 234.45
94
+ end
95
+
96
+ it "should support the date type" do
97
+ @column = Slither::Column.new(:date, 10, :type => :date)
98
+ dt = @column.parse('2009-08-22')
99
+ dt.should be_a(Date)
100
+ dt.to_s.should == '2009-08-22'
101
+ end
102
+
103
+ it "should use the format option with date type if available" do
104
+ @column = Slither::Column.new(:date, 10, :type => :date, :format => "%m%d%Y")
105
+ dt = @column.parse('08222009')
106
+ dt.should be_a(Date)
107
+ dt.to_s.should == '2009-08-22'
108
+ end
109
+ end
110
+
111
+ describe "when applying formatting options" do
112
+ it "should return a proper formatter" do
113
+ @column = Slither::Column.new(@name, @length, :align => :left)
114
+ @column.send(:formatter).should == "%-5s"
115
+ end
116
+
117
+ it "should respect a right alignment" do
118
+ @column = Slither::Column.new(@name, @length, :align => :right)
119
+ @column.format(25).should == ' 25'
120
+ end
121
+
122
+ it "should respect a left alignment" do
123
+ @column = Slither::Column.new(@name, @length, :align => :left)
124
+ @column.format(25).should == '25 '
125
+ end
126
+
127
+ it "should respect padding with spaces" do
128
+ @column = Slither::Column.new(@name, @length, :padding => :space)
129
+ @column.format(25).should == ' 25'
130
+ end
131
+
132
+ it "should respect padding with zeros with integer types" do
133
+ @column = Slither::Column.new(@name, @length, :type => :integer, :padding => :zero)
134
+ @column.format(25).should == '00025'
135
+ end
136
+
137
+ describe "that is a float type" do
138
+ it "should respect padding with zeros aligned right" do
139
+ @column = Slither::Column.new(@name, @length, :type => :float, :padding => :zero, :align => :right)
140
+ @column.format(4.45).should == '04.45'
141
+ end
142
+
143
+ it "should respect padding with zeros aligned left" do
144
+ @column = Slither::Column.new(@name, @length, :type => :float, :padding => :zero, :align => :left)
145
+ @column.format(4.45).should == '4.450'
146
+ end
147
+ end
148
+ end
149
+
150
+ describe "when formatting values for a file" do
151
+ it "should default to a string" do
152
+ @column = Slither::Column.new(:name, 10)
153
+ @column.format('Bill').should == ' Bill'
154
+ end
155
+
156
+ describe "whose size is too long" do
157
+ it "should raise an error if truncate is false" do
158
+ @value = "XX" * @length
159
+ lambda { @column.format(@value) }.should raise_error(
160
+ Slither::FormattedStringExceedsLengthError,
161
+ "The formatted value '#{@value}' in column '#{@name}' exceeds the allowed length of #{@length} chararacters."
162
+ )
163
+ end
164
+
165
+ it "should truncate from the left if truncate is true and aligned left" do
166
+ @column = Slither::Column.new(@name, @length, :truncate => true, :align => :left)
167
+ @column.format("This is too long").should == "This "
168
+ end
169
+
170
+ it "should truncate from the right if truncate is true and aligned right" do
171
+ @column = Slither::Column.new(@name, @length, :truncate => true, :align => :right)
172
+ @column.format("This is too long").should == " long"
173
+ end
174
+ end
175
+
176
+ it "should support the integer type" do
177
+ @column = Slither::Column.new(:amount, 10, :type => :integer)
178
+ @column.format(234).should == ' 234'
179
+ @column.format('234').should == ' 234'
180
+ end
181
+
182
+ it "should support the float type" do
183
+ @column = Slither::Column.new(:amount, 10, :type => :float)
184
+ @column.format(234.45).should == ' 234.45'
185
+ @column.format('234.4500').should == ' 234.45'
186
+ @column.format('3').should == ' 3.0'
187
+ end
188
+
189
+ it "should support the float type with a format" do
190
+ @column = Slither::Column.new(:amount, 10, :type => :float, :format => "%.3f")
191
+ @column.format(234.45).should == ' 234.450'
192
+ @column.format('234.4500').should == ' 234.450'
193
+ @column.format('3').should == ' 3.000'
194
+ end
195
+
196
+ it "should support the float type with a format, alignment and padding" do
197
+ @column = Slither::Column.new(:amount, 10, :type => :float, :format => "%.2f", :align => :left, :padding => :zero)
198
+ @column.format(234.45).should == '234.450000'
199
+ @column = Slither::Column.new(:amount, 10, :type => :float, :format => "%.2f", :align => :right, :padding => :zero)
200
+ @column.format('234.400').should == '0000234.40'
201
+ @column = Slither::Column.new(:amount, 10, :type => :float, :format => "%.4f", :align => :left, :padding => :space)
202
+ @column.format('3').should == '3.0000 '
203
+ end
204
+
205
+ it "should support the money_with_implied_decimal type" do
206
+ @column = Slither::Column.new(:amount, 10, :type=> :money_with_implied_decimal)
207
+ @column.format(234.450).should == " 23445"
208
+ @column.format(12.34).should == " 1234"
209
+ end
210
+
211
+ it "should support the date type" do
212
+ dt = Date.new(2009, 8, 22)
213
+ @column = Slither::Column.new(:date, 10, :type => :date)
214
+ @column.format(dt).should == '2009-08-22'
215
+ end
216
+
217
+ it "should support the date type with a :format" do
218
+ dt = Date.new(2009, 8, 22)
219
+ @column = Slither::Column.new(:date, 8, :type => :date, :format => "%m%d%Y")
220
+ @column.format(dt).should == '08222009'
221
+ end
222
+ end
223
+
224
+ end
@@ -0,0 +1,85 @@
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
+ section = mock('section', :null_object => true)
20
+ Slither::Section.should_receive(:new).with('name', {:align => :left}).and_return(section)
21
+ d = Slither::Definition.new
22
+ d.options[:align].should == :right
23
+ d.section('name', :align => :left) {}
24
+ end
25
+ end
26
+
27
+ describe "when creating a section" do
28
+ before(:each) do
29
+ @d = Slither::Definition.new
30
+ @section = mock('section', :null_object => true)
31
+ end
32
+
33
+ it "should create and yield a new section object" do
34
+ yielded = nil
35
+ @d.section :header do |section|
36
+ yielded = section
37
+ end
38
+ yielded.should be_a(Slither::Section)
39
+ @d.sections.first.should == yielded
40
+ end
41
+
42
+ it "should magically build a section from an unknown method" do
43
+ Slither::Section.should_receive(:new).with(:header, anything()).and_return(@section)
44
+ @d.header {}
45
+ end
46
+
47
+ it "should not create duplicate section names" do
48
+ lambda { @d.section(:header) {} }.should_not raise_error(ArgumentError)
49
+ lambda { @d.section(:header) {} }.should raise_error(ArgumentError, "Reserved or duplicate section name: 'header'")
50
+ end
51
+
52
+ it "should throw an error if a reserved section name is used" do
53
+ lambda { @d.section(:spacer) {} }.should raise_error(ArgumentError, "Reserved or duplicate section name: 'spacer'")
54
+ end
55
+ end
56
+
57
+ describe "when creating a template" do
58
+ before(:each) do
59
+ @d = Slither::Definition.new
60
+ @section = mock('section', :null_object => true)
61
+ end
62
+
63
+ it "should create a new section" do
64
+ Slither::Section.should_receive(:new).with(:row, anything()).and_return(@section)
65
+ @d.template(:row) {}
66
+ end
67
+
68
+ it "should yield the new section" do
69
+ Slither::Section.should_receive(:new).with(:row, anything()).and_return(@section)
70
+ yielded = nil
71
+ @d.template :row do |section|
72
+ yielded = section
73
+ end
74
+ yielded.should == @section
75
+ end
76
+
77
+ it "add a section to the templates collection" do
78
+ @d.should have(0).templates
79
+ @d.template :row do |t|
80
+ t.column :id, 3
81
+ end
82
+ @d.should have(1).templates
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,42 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe Slither::Generator do
4
+ before(:each) do
5
+ @definition = Slither.define :test do |d|
6
+ d.header do |h|
7
+ h.trap { |line| line[0,4] == 'HEAD' }
8
+ h.column :type, 4
9
+ h.column :file_id, 10
10
+ end
11
+ d.body do |b|
12
+ b.trap { |line| line[0,4] =~ /[^(HEAD|FOOT)]/ }
13
+ b.column :first, 10
14
+ b.column :last, 10
15
+ end
16
+ d.footer do |f|
17
+ f.trap { |line| line[0,4] == 'FOOT' }
18
+ f.column :type, 4
19
+ f.column :file_id, 10
20
+ end
21
+ end
22
+ @data = {
23
+ :header => [ {:type => "HEAD", :file_id => "1" }],
24
+ :body => [
25
+ {:first => "Paul", :last => "Hewson" },
26
+ {:first => "Dave", :last => "Evans" }
27
+ ],
28
+ :footer => [ {:type => "FOOT", :file_id => "1" }]
29
+ }
30
+ @generator = Slither::Generator.new(@definition)
31
+ end
32
+
33
+ it "should raise an error if there is no data for a required section" do
34
+ @data.delete :header
35
+ lambda { @generator.generate(@data) }.should raise_error(Slither::RequiredSectionEmptyError, "Required section 'header' was empty.")
36
+ end
37
+
38
+ it "should generate a string" do
39
+ expected = "HEAD 1\n Paul Hewson\n Dave Evans\nFOOT 1"
40
+ @generator.generate(@data).should == expected
41
+ end
42
+ end
@@ -0,0 +1,74 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe Slither::Parser do
4
+ before(:each) do
5
+ @definition = mock('definition', :sections => [])
6
+ @file = mock("file", :gets => nil)
7
+ @file_name = 'test.txt'
8
+ @parser = Slither::Parser.new(@definition, @file_name)
9
+ end
10
+
11
+ it "should open and yield the source file" do
12
+ File.should_receive(:open).with(@file_name, 'r').and_yield(@file)
13
+ @parser.parse
14
+ end
15
+
16
+ describe "when parsing sections" do
17
+ before(:each) do
18
+ @definition = Slither.define :test do |d|
19
+ d.header do |h|
20
+ h.trap { |line| line[0,4] == 'HEAD' }
21
+ h.column :type, 4
22
+ h.column :file_id, 10
23
+ end
24
+ d.body do |b|
25
+ b.trap { |line| line[0,4] =~ /[^(HEAD|FOOT)]/ }
26
+ b.column :first, 10
27
+ b.column :last, 10
28
+ end
29
+ d.footer do |f|
30
+ f.trap { |line| line[0,4] == 'FOOT' }
31
+ f.column :type, 4
32
+ f.column :file_id, 10
33
+ end
34
+ end
35
+ File.should_receive(:open).with(@file_name, 'r').and_yield(@file)
36
+ @parser = Slither::Parser.new(@definition, @file_name)
37
+ end
38
+
39
+ it "should add lines to the proper sections" do
40
+ @file.should_receive(:gets).exactly(4).times.and_return(
41
+ 'HEAD 1',
42
+ ' Paul Hewson',
43
+ ' Dave Evans',
44
+ 'FOOT 1',
45
+ nil
46
+ )
47
+ expected = {
48
+ :header => [ {:type => "HEAD", :file_id => "1" }],
49
+ :body => [
50
+ {:first => "Paul", :last => "Hewson" },
51
+ {:first => "Dave", :last => "Evans" }
52
+ ],
53
+ :footer => [ {:type => "FOOT", :file_id => "1" }]
54
+ }
55
+ result = @parser.parse
56
+ result.should == expected
57
+ end
58
+
59
+ it "should allow optional sections to be skipped" do
60
+ @definition.sections[0].optional = true
61
+ @definition.sections[2].optional = true
62
+ @file.should_receive(:gets).twice.and_return(' Paul Hewson', nil)
63
+ expected = { :body => [ {:first => "Paul", :last => "Hewson" } ] }
64
+ @parser.parse.should == expected
65
+ end
66
+
67
+ it "should raise an error if a required section is not found" do
68
+ @file.should_receive(:gets).twice.and_return(' Ryan Wood', nil)
69
+ lambda { @parser.parse }.should raise_error(Slither::RequiredSectionNotFoundError, "Required section 'header' was not found.")
70
+ end
71
+
72
+ # it "raise an error if a section limit is over run"
73
+ end
74
+ end
@@ -0,0 +1,146 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe Slither::Section do
4
+ before(:each) do
5
+ @section = Slither::Section.new(:body)
6
+ end
7
+
8
+ it "should have no columns after creation" do
9
+ @section.columns.should be_empty
10
+ end
11
+
12
+ it "should know it's reserved names" do
13
+ Slither::Section::RESERVED_NAMES.should == [:spacer]
14
+ end
15
+
16
+ describe "when adding columns" do
17
+ it "should build an ordered column list" do
18
+ @section.should have(0).columns
19
+
20
+ col1 = @section.column :id, 10
21
+ col2 = @section.column :name, 30
22
+ col3 = @section.column :state, 2
23
+
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
29
+
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
35
+
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
56
+ end
57
+
58
+ it "should accept and store the trap as a block" do
59
+ @section.trap { |v| v == 4 }
60
+ trap = @section.instance_variable_get(:@trap)
61
+ trap.should be_a(Proc)
62
+ trap.call(4).should == true
63
+ end
64
+
65
+ describe "when adding a template" do
66
+ before(:each) do
67
+ @template = mock('templated section', :columns => [1,2,3], :options => {})
68
+ @definition = mock("definition", :templates => { :test => @template } )
69
+ @section.definition = @definition
70
+ end
71
+
72
+ it "should ensure the template exists" do
73
+ @definition.stub! :templates => {}
74
+ lambda { @section.template(:none) }.should raise_error(ArgumentError)
75
+ end
76
+
77
+ it "should add the template columns to the current column list" do
78
+ @section.template :test
79
+ @section.should have(3).columns
80
+ end
81
+
82
+ it "should merge the template option" do
83
+ @section = Slither::Section.new(:body, :align => :left)
84
+ @section.definition = @definition
85
+ @template.stub! :options => {:align => :right}
86
+ @section.template :test
87
+ @section.options.should == {:align => :left}
88
+ end
89
+ end
90
+
91
+ describe "when formatting a row" do
92
+ before(:each) do
93
+ @data = { :id => 3, :name => "Ryan" }
94
+ end
95
+
96
+ it "should default to string data aligned right" do
97
+ @section.column(:id, 5)
98
+ @section.column(:name, 10)
99
+ @section.format( @data ).should == " 3 Ryan"
100
+ end
101
+
102
+ it "should left align if asked" do
103
+ @section.column(:id, 5)
104
+ @section.column(:name, 10, :align => :left)
105
+ @section.format(@data).should == " 3Ryan "
106
+ end
107
+
108
+ # it "should raise an error if the data and column definitions aren't the same size" do
109
+ # @section.column(:id, 5)
110
+ # lambda { @section.format(@data) }.should raise_error(
111
+ # Slither::ColumnMismatchError,
112
+ # "The 'body' section has 1 column(s) defined, but there are 2 column(s) provided in the data."
113
+ # )
114
+ # end
115
+ end
116
+
117
+ describe "when parsing a file" do
118
+ before(:each) do
119
+ @line = ' 45 Ryan WoodSC '
120
+ @section = Slither::Section.new(:body)
121
+ @column_content = { :id => 5, :first => 10, :last => 10, :state => 2 }
122
+ end
123
+
124
+ it "should return a key for key column" do
125
+ @column_content.each { |k,v| @section.column(k, v) }
126
+ parsed = @section.parse(@line)
127
+ @column_content.each_key { |name| parsed.should have_key(name) }
128
+ end
129
+
130
+ it "should not return a key for reserved names" do
131
+ @column_content.each { |k,v| @section.column(k, v) }
132
+ @section.spacer 5
133
+ @section.should have(5).columns
134
+ parsed = @section.parse(@line)
135
+ parsed.should have(4).keys
136
+ end
137
+ end
138
+
139
+ it "should try to match a line using the trap" do
140
+ @section.trap do |line|
141
+ line == 'hello'
142
+ end
143
+ @section.match('hello').should be_true
144
+ @section.match('goodbye').should be_false
145
+ end
146
+ end
@@ -0,0 +1,84 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe Slither do
4
+
5
+ before(:each) do
6
+ @name = :doc
7
+ @options = { :align => :left }
8
+ end
9
+
10
+ describe "when defining a format" do
11
+ before(:each) do
12
+ @definition = mock('definition')
13
+ end
14
+
15
+ it "should create a new definition using the specified name and options" do
16
+ Slither.should_receive(:define).with(@name, @options).and_return(@definition)
17
+ Slither.define(@name , @options)
18
+ end
19
+
20
+ it "should pass the definition to the block" do
21
+ yielded = nil
22
+ Slither.define(@name) do |y|
23
+ yielded = y
24
+ end
25
+ yielded.should be_a( Slither::Definition )
26
+ end
27
+
28
+ it "should add to the internal definition count" do
29
+ Slither.definitions.clear
30
+ Slither.should have(0).definitions
31
+ Slither.define(@name , @options) {}
32
+ Slither.should have(1).definitions
33
+ end
34
+ end
35
+
36
+ describe "when creating file from data" do
37
+ it "should raise an error if the definition name is not found" do
38
+ lambda { Slither.generate(:not_there, {}) }.should raise_error(ArgumentError)
39
+ end
40
+
41
+ it "should output a string" do
42
+ definition = mock('definition')
43
+ generator = mock('generator')
44
+ generator.should_receive(:generate).with({})
45
+ Slither.should_receive(:definition).with(:test).and_return(definition)
46
+ Slither::Generator.should_receive(:new).with(definition).and_return(generator)
47
+ Slither.generate(:test, {})
48
+ end
49
+
50
+ it "should output a file" do
51
+ file = mock('file')
52
+ text = mock('string')
53
+ file.should_receive(:write).with(text)
54
+ File.should_receive(:open).with('file.txt', 'w').and_yield(file)
55
+ Slither.should_receive(:generate).with(:test, {}).and_return(text)
56
+ Slither.write('file.txt', :test, {})
57
+ end
58
+ end
59
+
60
+ describe "when parsing a file" do
61
+ before(:each) do
62
+ @file_name = 'file.txt'
63
+ end
64
+
65
+ it "should check the file exists" do
66
+ lambda { Slither.parse(@file_name, :test, {}) }.should raise_error(ArgumentError)
67
+ end
68
+
69
+ it "should raise an error if the definition name is not found" do
70
+ Slither.definitions.clear
71
+ File.stub!(:exists? => true)
72
+ lambda { Slither.parse(@file_name, :test, {}) }.should raise_error(ArgumentError)
73
+ end
74
+
75
+ it "should create a parser and call parse" do
76
+ File.stub!(:exists? => true)
77
+ parser = mock("parser", :null_object => true)
78
+ definition = mock('definition')
79
+ Slither.should_receive(:definition).with(:test).and_return(definition)
80
+ Slither::Parser.should_receive(:new).with(definition, @file_name).and_return(parser)
81
+ Slither.parse(@file_name, :test)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'slither'))
4
+
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: slither-ruby19
3
+ version: !ruby/object:Gem::Version
4
+ hash: 405
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 99
9
+ - 3
10
+ version: 0.99.3
11
+ platform: ruby
12
+ authors:
13
+ - Ryan Wood
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2009-06-22 00:00:00 -05:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: bones
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 25
30
+ segments:
31
+ - 2
32
+ - 5
33
+ - 1
34
+ version: 2.5.1
35
+ type: :development
36
+ version_requirements: *id001
37
+ description: (Temporary fork of slither gem that works with ruby 1.9) A simple, clean DSL for describing, writing, and parsing fixed-width text files.
38
+ email: ryan.wood@gmail.com
39
+ executables: []
40
+
41
+ extensions: []
42
+
43
+ extra_rdoc_files:
44
+ - History.txt
45
+ - README.rdoc
46
+ files:
47
+ - History.txt
48
+ - README.rdoc
49
+ - Rakefile
50
+ - TODO
51
+ - lib/slither.rb
52
+ - lib/slither/column.rb
53
+ - lib/slither/definition.rb
54
+ - lib/slither/generator.rb
55
+ - lib/slither/parser.rb
56
+ - lib/slither/section.rb
57
+ - lib/slither/slither.rb
58
+ - slither.gemspec
59
+ - spec/column_spec.rb
60
+ - spec/definition_spec.rb
61
+ - spec/generator_spec.rb
62
+ - spec/parser_spec.rb
63
+ - spec/section_spec.rb
64
+ - spec/slither_spec.rb
65
+ - spec/spec_helper.rb
66
+ has_rdoc: true
67
+ homepage: http://github.com/ryanwood/slither
68
+ licenses: []
69
+
70
+ post_install_message:
71
+ rdoc_options:
72
+ - --main
73
+ - README.rdoc
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ hash: 3
82
+ segments:
83
+ - 0
84
+ version: "0"
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ hash: 3
91
+ segments:
92
+ - 0
93
+ version: "0"
94
+ requirements: []
95
+
96
+ rubyforge_project: !binary |
97
+ AA==
98
+
99
+ rubygems_version: 1.3.7
100
+ signing_key:
101
+ specification_version: 2
102
+ summary: (Temporary fork of slither gem that works with ruby 1.9) A simple, clean DSL for describing, writing, and parsing fixed-width text files
103
+ test_files: []
104
+