ryanwood-slither 0.99.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 0.99.0 / 2009-04-14
2
+
3
+ * Initial Release
4
+ * Happy Birthday!
data/README.rdoc ADDED
@@ -0,0 +1,106 @@
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
+
29
+ # The trap tells Slither which lines should fall into this section
30
+ header.trap do |line|
31
+ line[0,4] == 'HEAD'
32
+ end
33
+
34
+ # Use the boundary template for the columns
35
+ header.template :boundary
36
+ end
37
+
38
+ d.body do |body|
39
+ body.trap do |line|
40
+ line[0,4] =~ /[^(HEAD|FOOT)]/
41
+ end
42
+ body.column :id, 10, :type => :integer
43
+ body.column :name, 10, :align => :left
44
+ body.spacer 3
45
+ body.column :state, 2
46
+ end
47
+
48
+ d.footer, :, :limit => 1 do |footer|
49
+ footer.trap do |line|
50
+ line[0,4] == 'FOOT'
51
+ end
52
+ footer.template :boundary
53
+ footer.column :record_count, 10
54
+ end
55
+ end
56
+
57
+ Then either feed it a nested struct with data values to create the file in the defined format:
58
+
59
+ test_data = {
60
+ :body => [
61
+ { :id => 12, :name => "Ryan", :state => 'SC' },
62
+ { :id => 23, :name => "Joe", :state => 'VA' },
63
+ { :id => 42, :name => "Tommy", :state => 'FL' },
64
+ ],
65
+ :header => { :record_type => 'HEAD', :company_id => 'ABC' },
66
+ :footer => { :record_type => 'FOOT', :company_id => 'ABC' }
67
+ }
68
+
69
+ # Generates the file as a string
70
+ puts Slither.generate(:simple, test_data)
71
+
72
+ # Writes the file
73
+ Slither.write('outfile.txt', :simple, test_data)
74
+
75
+ or parse files already in that format into a nested hash:
76
+
77
+ parsed_data = Slither.parse('infile.txt', :test).inspect
78
+
79
+ == INSTALL:
80
+
81
+ sudo gem install slither
82
+
83
+ == LICENSE:
84
+
85
+ (The MIT License)
86
+
87
+ Copyright (c) 2008
88
+
89
+ Permission is hereby granted, free of charge, to any person obtaining
90
+ a copy of this software and associated documentation files (the
91
+ 'Software'), to deal in the Software without restriction, including
92
+ without limitation the rights to use, copy, modify, merge, publish,
93
+ distribute, sublicense, and/or sell copies of the Software, and to
94
+ permit persons to whom the Software is furnished to do so, subject to
95
+ the following conditions:
96
+
97
+ The above copyright notice and this permission notice shall be
98
+ included in all copies or substantial portions of the Software.
99
+
100
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
101
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
102
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
103
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
104
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
105
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
106
+ 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.0'
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,4 @@
1
+ * Validation
2
+ * Alternate Section Flow (other than linear), i.e. repeatable sections (think batch)
3
+ * Better Documentation
4
+ * Limit on section
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 0
3
+ :patch: 99
4
+ :major: 0
@@ -0,0 +1,62 @@
1
+ require 'date'
2
+
3
+ class Slither
4
+ class Column
5
+ attr_reader :name, :length, :alignment, :type, :options
6
+
7
+ def initialize(name, length, options = {})
8
+ assert_valid_options(options)
9
+ @name = name
10
+ @length = length
11
+ @options = options
12
+ @alignment = options[:align] || :right
13
+ @type = options[:type] || :string
14
+ end
15
+
16
+ def formatter
17
+ "%#{aligner}#{length}s"
18
+ end
19
+
20
+ def unpacker
21
+ "A#{@length}"
22
+ end
23
+
24
+ def to_type(value)
25
+ case @type
26
+ when :integer: value.to_i
27
+ when :float: value.to_f
28
+ when :date:
29
+ if @options[:date_format]
30
+ Date.strptime(value, @options[:date_format])
31
+ else
32
+ Date.strptime(value)
33
+ end
34
+ else value.strip
35
+ end
36
+ end
37
+
38
+ def format_string(value)
39
+ case @type
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
48
+ end
49
+
50
+ private
51
+
52
+ def aligner
53
+ @alignment == :left ? '-' : ''
54
+ end
55
+
56
+ def assert_valid_options(options)
57
+ unless options[:align].nil? || [:left, :right].include?(options[:align])
58
+ raise ArgumentError, "Option :align only accepts :right (default) or :left"
59
+ end
60
+ end
61
+ end
62
+ 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,29 @@
1
+ class Slither
2
+
3
+ class RequiredSectionEmptyError < StandardError; end
4
+
5
+ class Generator
6
+
7
+ def initialize(definition)
8
+ @definition = definition
9
+ end
10
+
11
+ def generate(data)
12
+ @builder = []
13
+ @definition.sections.each do |section|
14
+ content = data[section.name]
15
+ if content
16
+ content = [content] unless content.is_a?(Array)
17
+ raise Slither::RequiredSectionEmptyError if content.empty?
18
+ content.each do |row|
19
+ @builder << section.format(row)
20
+ end
21
+ else
22
+ raise Slither::RequiredSectionEmptyError unless section.optional
23
+ end
24
+ end
25
+ @builder.join("\n")
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,56 @@
1
+ class Slither
2
+
3
+ class RequiredSectionNotFoundError < StandardError; end
4
+
5
+ class Parser
6
+
7
+ def initialize(definition, file)
8
+ @definition = definition
9
+ @file = file
10
+ # This may be used in the future for non-linear or repeating sections
11
+ @mode = :linear
12
+ end
13
+
14
+ def parse()
15
+ @parsed = {}
16
+ @content = read_file
17
+ unless @content.empty?
18
+ @definition.sections.each do |section|
19
+ rows = fill_content(section)
20
+ raise Slither::RequiredSectionNotFoundError unless rows > 0 || section.optional
21
+ end
22
+ end
23
+ @parsed
24
+ end
25
+
26
+ private
27
+
28
+ def read_file
29
+ content = []
30
+ File.open(@file, 'r') do |f|
31
+ while (line = f.gets) do
32
+ content << line
33
+ end
34
+ end
35
+ content
36
+ end
37
+
38
+ def fill_content(section)
39
+ matches = 0
40
+ loop do
41
+ line = @content.first
42
+ break unless section.match(line)
43
+ add_to_section(section, line)
44
+ matches += 1
45
+ @content.shift
46
+ end
47
+ matches
48
+ end
49
+
50
+ def add_to_section(section, line)
51
+ @parsed[section.name] = [] unless @parsed[section.name]
52
+ @parsed[section.name] << section.parse(line)
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,66 @@
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
+ col = Column.new(name, length, @options.merge(options))
18
+ @columns << col
19
+ col
20
+ end
21
+
22
+ def spacer(length)
23
+ column(:spacer, length)
24
+ end
25
+
26
+ def trap(&block)
27
+ @trap = block
28
+ end
29
+
30
+ def template(name)
31
+ template = @definition.templates[name]
32
+ raise ArgumentError, "Template #{name} not found as a known template." unless template
33
+ @columns = @columns + template.columns
34
+ # Section options should trump template options
35
+ @options = template.options.merge(@options)
36
+ end
37
+
38
+ def format(data)
39
+ row = ''
40
+ @columns.each do |column|
41
+ row += (column.formatter % column.format_string(data[column.name]))
42
+ end
43
+ row
44
+ end
45
+
46
+ def parse(line)
47
+ line_data = line.unpack(unpacker)
48
+ row = {}
49
+ @columns.each_with_index do |c, i|
50
+ row[c.name] = c.to_type(line_data[i]) unless RESERVED_NAMES.include?(c.name)
51
+ end
52
+ row
53
+ end
54
+
55
+ def match(raw_line)
56
+ raw_line.nil? ? false : @trap.call(raw_line)
57
+ end
58
+
59
+ private
60
+
61
+ def unpacker
62
+ @columns.map { |c| c.unpacker }.join('')
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,42 @@
1
+ class Slither
2
+
3
+ VERSION = '0.99.0'
4
+
5
+ def self.define(name, options = {}, &block)
6
+ definition = Definition.new(options)
7
+ yield(definition)
8
+ definitions[name] = definition
9
+ definition
10
+ end
11
+
12
+ def self.generate(definition_name, data)
13
+ definition = definition(definition_name)
14
+ raise ArgumentError, "Definition name '#{name}' was not found." unless definition
15
+ generator = Generator.new(definition)
16
+ generator.generate(data)
17
+ end
18
+
19
+ def self.write(filename, definition_name, data)
20
+ File.open(filename, 'w') do |f|
21
+ f.write generate(definition_name, data)
22
+ end
23
+ end
24
+
25
+ def self.parse(filename, definition_name)
26
+ raise ArgumentError, "File #{filename} does not exist." unless File.exists?(filename)
27
+ definition = definition(definition_name)
28
+ raise ArgumentError, "Definition name '#{definition_name}' was not found." unless definition
29
+ parser = Parser.new(definition, filename)
30
+ parser.parse
31
+ end
32
+
33
+ private
34
+
35
+ def self.definitions
36
+ @@definitions ||= {}
37
+ end
38
+
39
+ def self.definition(name)
40
+ definitions[name]
41
+ end
42
+ end
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'
data/slither.gemspec ADDED
Binary file
@@ -0,0 +1,107 @@
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 alignment" do
20
+ @column.alignment.should == :right
21
+ end
22
+
23
+ it "should return a proper formatter" do
24
+ @column.formatter.should == "%5s"
25
+ end
26
+ end
27
+
28
+ describe "when specifying an alignment" do
29
+ before(:each) do
30
+ @column = Slither::Column.new(@name, @length, :align => :left)
31
+ end
32
+
33
+ it "should only accept :right or :left for an alignment" do
34
+ lambda{ Slither::Column.new(@name, @length, :align => :bogus) }.should raise_error(ArgumentError, "Option :align only accepts :right (default) or :left")
35
+ end
36
+
37
+ it "should override the default alignment" do
38
+ @column.alignment.should == :left
39
+ end
40
+
41
+ it "should return a proper formatter" do
42
+ @column.formatter.should == "%-5s"
43
+ end
44
+ end
45
+
46
+ it "should return the proper unpack value for a string" do
47
+ @column.unpacker.should == 'A5'
48
+ end
49
+
50
+ describe "when typing the value" do
51
+ it "should default to a string" do
52
+ @column.to_type('name').should == 'name'
53
+ end
54
+
55
+ it "should support the :integer type" do
56
+ @column = Slither::Column.new(@name, @length, :type => :integer)
57
+ @column.to_type('234').should == 234
58
+ end
59
+
60
+ it "should support the :float type" do
61
+ @column = Slither::Column.new(@name, @length, :type => :float)
62
+ @column.to_type('234.45').should == 234.45
63
+ end
64
+
65
+ it "should support the :date type" do
66
+ @column = Slither::Column.new(@name, @length, :type => :date)
67
+ dt = @column.to_type('2009-08-22')
68
+ dt.should be_a(Date)
69
+ dt.to_s.should == '2009-08-22'
70
+ end
71
+
72
+ it "should use the :date_format option with :date type if available" do
73
+ @column = Slither::Column.new(@name, @length, :type => :date, :date_format => "%m%d%Y")
74
+ dt = @column.to_type('08222009')
75
+ dt.should be_a(Date)
76
+ dt.to_s.should == '2009-08-22'
77
+ end
78
+ end
79
+
80
+ describe "when formatting the value" do
81
+ it "should default to a string" do
82
+ @column.format_string('name').should == 'name'
83
+ end
84
+
85
+ it "should support the :integer type" do
86
+ @column = Slither::Column.new(@name, @length, :type => :integer)
87
+ @column.format_string(234).should == '234'
88
+ end
89
+
90
+ it "should support the :float type" do
91
+ @column = Slither::Column.new(@name, @length, :type => :float)
92
+ @column.format_string(234.45).should == '234.45'
93
+ end
94
+
95
+ it "should support the :date type" do
96
+ dt = Date.new(2009, 8, 22)
97
+ @column = Slither::Column.new(@name, @length, :type => :date)
98
+ @column.format_string(dt).should == '2009-08-22'
99
+ end
100
+
101
+ it "should use the :date_format option with :date type if available" do
102
+ dt = Date.new(2009, 8, 22)
103
+ @column = Slither::Column.new(@name, @length, :type => :date, :date_format => "%m%d%Y")
104
+ @column.format_string(dt).should == '08222009'
105
+ end
106
+ end
107
+ 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)
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,83 @@
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)
70
+ end
71
+
72
+ it "raise an error if a section limit is over run"
73
+ end
74
+
75
+
76
+
77
+ describe "when in linear mode" do
78
+
79
+ end
80
+
81
+
82
+
83
+ end
@@ -0,0 +1,120 @@
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
+ it "should build an ordered column list" do
17
+ @section.should have(0).columns
18
+
19
+ col1 = @section.column :id, 10
20
+ col2 = @section.column :name, 30
21
+ col3 = @section.column :state, 2
22
+
23
+ @section.should have(3).columns
24
+ @section.columns[0].should be(col1)
25
+ @section.columns[1].should be(col2)
26
+ @section.columns[2].should be(col3)
27
+ end
28
+
29
+ it "should create spacer columns" do
30
+ @section.should have(0).columns
31
+ @section.spacer(5)
32
+ @section.should have(1).columns
33
+ end
34
+
35
+ it "can should override the alignment of the definition" do
36
+ section = Slither::Section.new('name', :align => :left)
37
+ section.options[:align].should == :left
38
+ end
39
+
40
+ it "should accept and store the trap as a block" do
41
+ @section.trap { |v| v == 4 }
42
+ trap = @section.instance_variable_get(:@trap)
43
+ trap.should be_a(Proc)
44
+ trap.call(4).should == true
45
+ end
46
+
47
+ describe "when adding a template" do
48
+ before(:each) do
49
+ @template = mock('templated section', :columns => [1,2,3], :options => {})
50
+ @definition = mock("definition", :templates => { :test => @template } )
51
+ @section.definition = @definition
52
+ end
53
+
54
+ it "should ensure the template exists" do
55
+ @definition.stub! :templates => {}
56
+ lambda { @section.template(:none) }.should raise_error(ArgumentError)
57
+ end
58
+
59
+ it "should add the template columns to the current column list" do
60
+ @section.template :test
61
+ @section.should have(3).columns
62
+ end
63
+
64
+ it "should merge the template option" do
65
+ @section = Slither::Section.new(:body, :align => :left)
66
+ @section.definition = @definition
67
+ @template.stub! :options => {:align => :right}
68
+ @section.template :test
69
+ @section.options.should == {:align => :left}
70
+ end
71
+ end
72
+
73
+ describe "when formatting a row" do
74
+ before(:each) do
75
+ @data = { :id => 3, :name => "Ryan" }
76
+ end
77
+
78
+ it "should default to string data aligned right" do
79
+ @section.column(:id, 5)
80
+ @section.column(:name, 10)
81
+ @section.format( @data ).should == " 3 Ryan"
82
+ end
83
+
84
+ it "should left align if asked" do
85
+ @section.column(:id, 5)
86
+ @section.column(:name, 10, :align => :left)
87
+ @section.format(@data).should == " 3Ryan "
88
+ end
89
+ end
90
+
91
+ describe "when parsing a file" do
92
+ before(:each) do
93
+ @line = ' 45 Ryan WoodSC '
94
+ @section = Slither::Section.new(:body)
95
+ @column_content = { :id => 5, :first => 10, :last => 10, :state => 2 }
96
+ end
97
+
98
+ it "should return a key for key column" do
99
+ @column_content.each { |k,v| @section.column(k, v) }
100
+ parsed = @section.parse(@line)
101
+ @column_content.each_key { |name| parsed.should have_key(name) }
102
+ end
103
+
104
+ it "should not return a key for reserved names" do
105
+ @column_content.each { |k,v| @section.column(k, v) }
106
+ @section.spacer 5
107
+ @section.should have(5).columns
108
+ parsed = @section.parse(@line)
109
+ parsed.should have(4).keys
110
+ end
111
+ end
112
+
113
+ it "should try to match a line using the trap" do
114
+ @section.trap do |line|
115
+ line == 'hello'
116
+ end
117
+ @section.match('hello').should be_true
118
+ @section.match('goodbye').should be_false
119
+ end
120
+ 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,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ryanwood-slither
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.99.0
5
+ platform: ruby
6
+ authors:
7
+ - Ryan Wood
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-04-17 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: bones
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 2.5.0
24
+ version:
25
+ description: A simple, clean DSL for describing, writing, and parsing fixed-width text files.
26
+ email: ryan.wood@gmail.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - History.txt
33
+ - README.rdoc
34
+ files:
35
+ - History.txt
36
+ - README.rdoc
37
+ - Rakefile
38
+ - TODO
39
+ - VERSION.yml
40
+ - lib/slither.rb
41
+ - lib/slither/column.rb
42
+ - lib/slither/definition.rb
43
+ - lib/slither/generator.rb
44
+ - lib/slither/parser.rb
45
+ - lib/slither/section.rb
46
+ - lib/slither/slither.rb
47
+ - slither.gemspec
48
+ - spec/column_spec.rb
49
+ - spec/definition_spec.rb
50
+ - spec/generator_spec.rb
51
+ - spec/parser_spec.rb
52
+ - spec/section_spec.rb
53
+ - spec/slither_spec.rb
54
+ - spec/spec_helper.rb
55
+ has_rdoc: true
56
+ homepage: http://github.com/ryanwood/slither
57
+ post_install_message:
58
+ rdoc_options:
59
+ - --main
60
+ - README.rdoc
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ version:
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: "0"
74
+ version:
75
+ requirements: []
76
+
77
+ rubyforge_project: !binary |
78
+ AA==
79
+
80
+ rubygems_version: 1.2.0
81
+ signing_key:
82
+ specification_version: 2
83
+ summary: A simple, clean DSL for describing, writing, and parsing fixed-width text files
84
+ test_files: []
85
+