ryanwood-slither 0.99.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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
+