Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Change FixedWidth methods to accept an IO instance instead of a filen…
…ame.

* not our responsibility to open/close IO
* will make IO handling more generic (StringIO for testing, especially.)
* clean up README and add example in README to examples/readme_example.rb
* update specs to reflect
  • Loading branch information
Timon Karnezos committed May 23, 2010
1 parent b3908ba commit 2c6134a
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 48 deletions.
41 changes: 24 additions & 17 deletions README.markdown
Expand Up @@ -15,33 +15,35 @@ SYNOPSIS:

# Create a FixedWidth::Defintion to describe a file format
FixedWidth.define :simple do |d|

# This is a template section that can be reused in other sections
d.template :boundary do |t|
t.column :record_type, 4
t.column :company_id, 12
end

# Create a header section
d.header, :align => :left do |header|
d.header(:align => :left) do |header|
# The trap tells FixedWidth which lines should fall into this section
header.trap { |line| line[0,4] == 'HEAD' }
# Use the boundary template for the columns
header.template :boundary
end

d.body do |body|
body.trap { |line| line[0,4] =~ /[^(HEAD|FOOT)]/ }
body.column :id, 10, :parser => :to_i
body.column :name, 10, :align => :left
body.column :first, 10, :align => :left, :group => :name
body.column :last, 10, :align => :left, :group => :name
body.spacer 3
body.column :state, 2
body.column :city, 20 , :group => :address
body.column :state, 2 , :group => :address
body.column :country, 3, :group => :address
end

d.footer do |footer|
footer.trap { |line| line[0,4] == 'FOOT' }
footer.template :boundary
footer.column :record_count, 10
footer.column :record_count, 10, :parser => :to_i
end
end

Expand All @@ -50,24 +52,29 @@ SYNOPSIS:
Then either feed it a nested struct with data values to create the file in the defined format:

test_data = {
:body => [
{ :id => 12, :name => "Ryan", :state => 'SC' },
{ :id => 23, :name => "Joe", :state => 'VA' },
{ :id => 42, :name => "Tommy", :state => 'FL' },
],
:header => { :record_type => 'HEAD', :company_id => 'ABC' },
:footer => { :record_type => 'FOOT', :company_id => 'ABC' }
:body => [
{ :id => 12,
:name => { :first => "Ryan", :last => "Wood" },
:address => { :city => "Foo", :state => 'SC', :country => "USA" }
},
{ :id => 13,
:name => { :first => "Jo", :last => "Schmo" },
:address => { :city => "Bar", :state => "CA", :country => "USA" }
}
],
:header => [{ :record_type => 'HEAD', :company_id => 'ABC' }],
:footer => [{ :record_type => 'FOOT', :company_id => 'ABC', :record_count => 2 }]
}

# Generates the file as a string
puts FixedWidth.generate(:simple, test_data)

# Writes the file
FixedWidth.write('outfile.txt', :simple, test_data)
FixedWidth.write(file_instance, :simple, test_data)

Or parse files already in that format into a nested hash:

parsed_data = FixedWidth.parse('infile.txt', :test).inspect
parsed_data = FixedWidth.parse(file_instance, :test).inspect

INSTALL:
========
Expand Down
62 changes: 62 additions & 0 deletions examples/readme_example.rb
@@ -0,0 +1,62 @@
require 'stringio'
require File.join(File.dirname(__FILE__), "..", "lib", "fixed_width")

# Create a FixedWidth::Defintion to describe a file format
FixedWidth.define :simple do |d|
# This is a template section that can be reused in other sections
d.template :boundary do |t|
t.column :record_type, 4
t.column :company_id, 12
end

# Create a header section
d.header(:align => :left) do |header|
# The trap tells FixedWidth which lines should fall into this section
header.trap { |line| line[0,4] == 'HEAD' }
# Use the boundary template for the columns
header.template :boundary
end

d.body do |body|
body.trap { |line| line[0,4] =~ /[^(HEAD|FOOT)]/ }
body.column :id, 10, :parser => :to_i
body.column :first, 10, :align => :left, :group => :name
body.column :last, 10, :align => :left, :group => :name
body.spacer 3
body.column :city, 20 , :group => :address
body.column :state, 2 , :group => :address
body.column :country, 3, :group => :address
end

d.footer do |footer|
footer.trap { |line| line[0,4] == 'FOOT' }
footer.template :boundary
footer.column :record_count, 10, :parser => :to_i
end
end

test_data = {
:body => [
{ :id => 12,
:name => { :first => "Ryan", :last => "Wood" },
:address => { :city => "Foo", :state => 'SC', :country => "USA" }
},
{ :id => 13,
:name => { :first => "Jo", :last => "Schmo" },
:address => { :city => "Bar", :state => "CA", :country => "USA" }
}
],
:header => [{ :record_type => 'HEAD', :company_id => 'ABC' }],
:footer => [{ :record_type => 'FOOT', :company_id => 'ABC', :record_count => 2 }]
}

# Generates the file as a string
generated = FixedWidth.generate(:simple, test_data)

sio = StringIO.new
sio.write(generated)
sio.rewind

parsed = FixedWidth.parse(sio, :simple)

puts parsed == test_data
11 changes: 4 additions & 7 deletions lib/fixed_width/fixed_width.rb
Expand Up @@ -21,17 +21,14 @@ def self.generate(definition_name, data)
generator.generate(data)
end

def self.write(filename, definition_name, data)
File.open(filename, 'w') do |f|
f.write(generate(definition_name, data))
end
def self.write(file, definition_name, data)
file.write(generate(definition_name, data))
end

def self.parse(filename, definition_name)
raise ArgumentError.new("File #{filename} does not exist.") unless File.exists?(filename)
def self.parse(file, definition_name)
definition = definition(definition_name)
raise ArgumentError.new("Definition name '#{definition_name}' was not found.") unless definition
parser = Parser.new(definition, filename)
parser = Parser.new(definition, file)
parser.parse
end

Expand Down
2 changes: 1 addition & 1 deletion lib/fixed_width/parser.rb
Expand Up @@ -20,7 +20,7 @@ def parse
private

def read_file
File.readlines(@file).map(&:chomp)
@file.readlines.map(&:chomp)
end

def fill_content(section)
Expand Down
15 changes: 6 additions & 9 deletions spec/fixed_width_spec.rb
Expand Up @@ -51,34 +51,31 @@
file = mock('file')
text = mock('string')
file.should_receive(:write).with(text)
File.should_receive(:open).with('file.txt', 'w').and_yield(file)
FixedWidth.should_receive(:generate).with(:test, {}).and_return(text)
FixedWidth.write('file.txt', :test, {})
FixedWidth.write(file, :test, {})
end
end

describe "when parsing a file" do
before(:each) do
@file_name = 'file.txt'
@file = mock('file')
end

it "should check the file exists" do
lambda { FixedWidth.parse(@file_name, :test, {}) }.should raise_error(ArgumentError)
lambda { FixedWidth.parse(@file, :test, {}) }.should raise_error(ArgumentError)
end

it "should raise an error if the definition name is not found" do
FixedWidth.definitions.clear
File.stub!(:exists? => true)
lambda { FixedWidth.parse(@file_name, :test, {}) }.should raise_error(ArgumentError)
lambda { FixedWidth.parse(@file, :test, {}) }.should raise_error(ArgumentError)
end

it "should create a parser and call parse" do
File.stub!(:exists? => true)
parser = mock("parser", :null_object => true)
definition = mock('definition')
FixedWidth.should_receive(:definition).with(:test).and_return(definition)
FixedWidth::Parser.should_receive(:new).with(definition, @file_name).and_return(parser)
FixedWidth.parse(@file_name, :test)
FixedWidth::Parser.should_receive(:new).with(definition, @file).and_return(parser)
FixedWidth.parse(@file, :test)
end
end
end
27 changes: 13 additions & 14 deletions spec/parser_spec.rb
Expand Up @@ -3,13 +3,12 @@
describe FixedWidth::Parser do
before(:each) do
@definition = mock('definition', :sections => [])
@file = mock("file", :gets => nil)
@file_name = 'test.txt'
@parser = FixedWidth::Parser.new(@definition, @file_name)
@file = mock("file")
@parser = FixedWidth::Parser.new(@definition, @file)
end

it "should read in a source file" do
File.should_receive(:readlines).with(@file_name).and_return([""])
@file.should_receive(:readlines).and_return(["\n"])
@parser.parse
end

Expand All @@ -32,15 +31,15 @@
f.column :file_id, 10
end
end
@parser = FixedWidth::Parser.new(@definition, @file_name)
@parser = FixedWidth::Parser.new(@definition, @file)
end

it "should add lines to the proper sections" do
File.should_receive(:readlines).with(@file_name).and_return([
'HEAD 1',
' Paul Hewson',
' Dave Evans',
'FOOT 1'
@file.should_receive(:readlines).and_return([
"HEAD 1\n",
" Paul Hewson\n",
" Dave Evans\n",
"FOOT 1\n"
])
expected = {
:header => [ {:type => "HEAD", :file_id => "1" }],
Expand All @@ -57,16 +56,16 @@
it "should allow optional sections to be skipped" do
@definition.sections[0].optional = true
@definition.sections[2].optional = true
File.should_receive(:readlines).with(@file_name).and_return([
' Paul Hewson'
@file.should_receive(:readlines).and_return([
" Paul Hewson\n"
])
expected = { :body => [ {:first => "Paul", :last => "Hewson" } ] }
@parser.parse.should == expected
end

it "should raise an error if a required section is not found" do
File.should_receive(:readlines).with(@file_name).and_return([
' Ryan Wood'
@file.should_receive(:readlines).and_return([
" Ryan Wood\n"
])
lambda { @parser.parse }.should raise_error(FixedWidth::RequiredSectionNotFoundError, "Required section 'header' was not found.")
end
Expand Down

0 comments on commit 2c6134a

Please sign in to comment.