flat 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +14 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +15 -0
- data/Gemfile +4 -0
- data/Guardfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +106 -0
- data/Rakefile +18 -0
- data/flat.gemspec +36 -0
- data/lib/flat/errors.rb +33 -0
- data/lib/flat/field.rb +171 -0
- data/lib/flat/file.rb +18 -0
- data/lib/flat/file_data.rb +123 -0
- data/lib/flat/layout.rb +30 -0
- data/lib/flat/read_operations.rb +50 -0
- data/lib/flat/record.rb +99 -0
- data/lib/flat/version.rb +3 -0
- data/lib/flat.rb +79 -0
- data/spec/flat_file_helper.rb +27 -0
- data/spec/lib/flat/field_spec.rb +126 -0
- data/spec/lib/flat/file_data_spec.rb +46 -0
- data/spec/lib/flat/file_spec.rb +5 -0
- data/spec/lib/flat/record_spec.rb +40 -0
- data/spec/lib/flat_spec.rb +28 -0
- data/spec/spec_helper.rb +17 -0
- metadata +247 -0
data/lib/flat/record.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
##
|
2
|
+
# = Record
|
3
|
+
#
|
4
|
+
# A record abstracts on line or 'record' of a fixed width field.
|
5
|
+
# The methods available are the keys of the hash passed to the constructor.
|
6
|
+
# For example the call:
|
7
|
+
#
|
8
|
+
# h = Hash['first_name','Andy','status','Supercool!']
|
9
|
+
# r = Record::Definition.new(h)
|
10
|
+
#
|
11
|
+
# would respond to r.first_name, and r.status yielding
|
12
|
+
# 'Andy' and 'Supercool!' respectively.
|
13
|
+
#
|
14
|
+
#
|
15
|
+
module Record
|
16
|
+
module ClassMethods #:nodoc:
|
17
|
+
end # => module ClassMethods
|
18
|
+
|
19
|
+
##
|
20
|
+
# = Instance Methods
|
21
|
+
#
|
22
|
+
# Defines behavior for instances of a subclass of Flat::File regarding the
|
23
|
+
# creating of Records from a line of text from a flat file.
|
24
|
+
#
|
25
|
+
module InstanceMethods
|
26
|
+
|
27
|
+
##
|
28
|
+
# create a record from line. The line is one line (or record) read from the
|
29
|
+
# text file. The resulting record is an object which. The object takes signals
|
30
|
+
# for each field according to the various fields defined with add_field or
|
31
|
+
# varients of it.
|
32
|
+
#
|
33
|
+
# line_number is an optional line number of the line in a file of records.
|
34
|
+
# If line is not in a series of records (lines), omit and it'll be -1 in the
|
35
|
+
# resulting record objects. Just make sure you realize this when reporting
|
36
|
+
# errors.
|
37
|
+
#
|
38
|
+
# Both a getter (field_name), and setter (field_name=) are available to the
|
39
|
+
# user.
|
40
|
+
#
|
41
|
+
#--
|
42
|
+
# NOTE: No line length checking here; consider making protected
|
43
|
+
#++
|
44
|
+
#
|
45
|
+
def create_record line, line_number = -1
|
46
|
+
attributes = {}
|
47
|
+
values = line.unpack pack_format # Parse the incoming line
|
48
|
+
fields.each_with_index do |field, index|
|
49
|
+
attributes[field.name] = field.filter values[index]
|
50
|
+
end
|
51
|
+
Record::Definition.new self.class, attributes, line_number
|
52
|
+
end
|
53
|
+
|
54
|
+
end # => module InstanceMethods
|
55
|
+
|
56
|
+
def self.included receiver #:nodoc:
|
57
|
+
receiver.extend ClassMethods
|
58
|
+
receiver.send :include, InstanceMethods
|
59
|
+
end
|
60
|
+
|
61
|
+
# = Definition
|
62
|
+
#
|
63
|
+
# Defines the behavior of a Record.
|
64
|
+
#
|
65
|
+
class Definition #:nodoc:
|
66
|
+
attr_reader :parent, :attributes, :line_number
|
67
|
+
|
68
|
+
#
|
69
|
+
# Create a new Record from a Hash of attributes.
|
70
|
+
#
|
71
|
+
def initialize parent, attributes = {}, line_number = -1
|
72
|
+
@parent, @attributes, @line_number = parent, attributes, line_number
|
73
|
+
|
74
|
+
@attributes = parent.fields.inject({}) do |map, field|
|
75
|
+
map.update(field.name => attributes[field.name])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
#
|
80
|
+
# Catches method calls and returns field values or raises an Error.
|
81
|
+
#
|
82
|
+
def method_missing method, params = nil
|
83
|
+
if method.to_s =~ /^(.*)=$/
|
84
|
+
if attributes.has_key?($1.to_sym)
|
85
|
+
@attributes.store($1.to_sym, params)
|
86
|
+
else
|
87
|
+
raise Errors::FlatFileError, "Unknown method: #{method}"
|
88
|
+
end
|
89
|
+
else
|
90
|
+
if attributes.has_key?(method)
|
91
|
+
@attributes.fetch(method)
|
92
|
+
else
|
93
|
+
raise Errors::FlatFileError, "Unknown method: #{method}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end # => class Definition
|
98
|
+
|
99
|
+
end # => module Record
|
data/lib/flat/version.rb
ADDED
data/lib/flat.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'extlib'
|
2
|
+
|
3
|
+
require 'flat/version'
|
4
|
+
require 'flat/file'
|
5
|
+
|
6
|
+
# = Flat
|
7
|
+
#
|
8
|
+
# Flat files are typically plain/text files containing many lines of text, or
|
9
|
+
# data. Each line, or record, consists of one or more fields. However, unlike
|
10
|
+
# CSV (comma spearated value) files, there are no delimiters to define where
|
11
|
+
# values end or begin. Flat provides a mechaism of defining a file's record
|
12
|
+
# structure, allowing users to iterate over a given file and access its data as
|
13
|
+
# typical Ruby objects (String, Numeric, Date, Booleans, etc.).
|
14
|
+
#
|
15
|
+
# == Specification
|
16
|
+
#
|
17
|
+
# A flat file's specification is defined within the subclass of Flat::File. The
|
18
|
+
# use of <tt>add_field</tt> and <tt>pad</tt> define and document the record
|
19
|
+
# structure.
|
20
|
+
#
|
21
|
+
# Given the following
|
22
|
+
# # Actual plain text, flat file data, 29 bytes
|
23
|
+
# #
|
24
|
+
# # 10 20
|
25
|
+
# # 012345678901234567890123456789
|
26
|
+
# # Walt Whitman 18190531
|
27
|
+
# # Linus Torvalds 19691228
|
28
|
+
#
|
29
|
+
# class People < Flat::File
|
30
|
+
# add_field :first_name, width: 10, filter: :trim
|
31
|
+
# add_field :last_name, width: 10, filter: ->(v) { v.strip }
|
32
|
+
# add_field :birthday, width: 8, filter: BirthdayFilter
|
33
|
+
# pad :autoname, width: 2
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# You will notice the minimum required information is field name and width. The
|
37
|
+
# special case is with <tt>pad</tt>; you can specifiy a name but the general
|
38
|
+
# approach is to let Flat::File name it for you.
|
39
|
+
#
|
40
|
+
# An alternate method of specifying fields is to pass a block to the
|
41
|
+
# <tt>add_field</tt> method. When using the block method you do not have to
|
42
|
+
# specifiy the name first. However, you do need to set the name inside the
|
43
|
+
# block. The value yieled to the block is an instance of Field::Definition.
|
44
|
+
#
|
45
|
+
# class People < FlatFile
|
46
|
+
# add_field do |fd|
|
47
|
+
# fd.name = :first_name
|
48
|
+
# fd.width = 10
|
49
|
+
# fd.add_filter ->(v) { v.strip }
|
50
|
+
# fd.add_formatter ->(v) { v.strip }
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# add_field :last_name do |fd|
|
54
|
+
# fd.width = 10
|
55
|
+
# fd.add_filter ->(v) { v.strip }
|
56
|
+
# fd.add_formatter ->(v) { v.strip }
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# == Reading Data
|
62
|
+
#
|
63
|
+
# == Writing Data
|
64
|
+
#
|
65
|
+
# == Filters
|
66
|
+
#
|
67
|
+
# == Formatters
|
68
|
+
#
|
69
|
+
# == Exceptions
|
70
|
+
#
|
71
|
+
# * +FlatFileError+ - Generic error class and superclass of all other errors raised by Flat.
|
72
|
+
# * +LayoutConstructorError+ - The specified layout definition was not valid.
|
73
|
+
# * +RecordLengthError+ - Generic error having to do with line lengths not meeting expectations.
|
74
|
+
# * +ShortRecordError+ - The incoming line was shorter than expections defined.
|
75
|
+
# * +LongRecordError+ - The incoming line was longer than expections defined.
|
76
|
+
#
|
77
|
+
module Flat
|
78
|
+
include Extlib
|
79
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# This file contains Flat::File definitions that can be used to test the
|
2
|
+
# various features and functions of Flat.
|
3
|
+
|
4
|
+
class PersonFile < Flat::File
|
5
|
+
|
6
|
+
EXAMPLE_FILE = <<-EOF
|
7
|
+
1234567890123456789012345678901234567890
|
8
|
+
f_name l_name age pad---
|
9
|
+
Captain Stubing 4 xxx
|
10
|
+
No Phone 5 xxx
|
11
|
+
Has Phone 11111111116 xxx
|
12
|
+
|
13
|
+
EOF
|
14
|
+
|
15
|
+
add_field :f_name, :width => 10
|
16
|
+
|
17
|
+
add_field :l_name, :width => 10, :aggressive => true
|
18
|
+
|
19
|
+
add_field :phone, :width => 10
|
20
|
+
|
21
|
+
add_field :age, :width => 4, :filter => proc { |v| v.to_i }, :formatter => proc { |v| v.to_f.to_s }
|
22
|
+
|
23
|
+
pad :auto_name, :width => 3
|
24
|
+
|
25
|
+
add_field :ignore, :width => 3, :padding => true
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Field do
|
4
|
+
|
5
|
+
let(:flat_file) { Flat::File }
|
6
|
+
|
7
|
+
describe Field::Definition do
|
8
|
+
describe 'instance creation' do
|
9
|
+
it 'should add a new field definition given minimum required information' do
|
10
|
+
# Minimum required fields are not enforced, just 'nice to have'.
|
11
|
+
field = flat_file.add_field :name, width: 5
|
12
|
+
expect( field ).to be_an_instance_of( Field::Definition )
|
13
|
+
expect( field.parent ).to eq( flat_file )
|
14
|
+
expect( field.name ).to eq( :name )
|
15
|
+
expect( field.width ).to eq( 5 )
|
16
|
+
expect( field.padding? ).to be false
|
17
|
+
expect( field.aggressive? ).to be false
|
18
|
+
expect( field.filters ).to be_empty
|
19
|
+
expect( field.formatters ).to be_empty
|
20
|
+
expect( field.map_in_proc ).to be_nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end # => describe Field::Definition
|
24
|
+
|
25
|
+
describe 'add_field' do
|
26
|
+
before do
|
27
|
+
flat_file.reset_file_data
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'adds 1 field to flat file' do
|
31
|
+
flat_file.add_field :test, width: 20
|
32
|
+
expect( flat_file.fields.size ).to eq( 1 )
|
33
|
+
expect( flat_file.width ).to eq( 20 )
|
34
|
+
expect( flat_file.pack_format ).to eq( 'A20' )
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'adds 2 fields to flat file' do
|
38
|
+
flat_file.add_field :test, width: 17
|
39
|
+
flat_file.add_field :test, width: 23
|
40
|
+
expect( flat_file.fields.size ).to eq( 2 )
|
41
|
+
expect( flat_file.width ).to eq( 40 )
|
42
|
+
expect( flat_file.pack_format ).to eq( 'A17A23' )
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'adds a field via a block' do
|
46
|
+
# Method A: specifying field name inside the block
|
47
|
+
flat_file.add_field do |field|
|
48
|
+
field.name = :test
|
49
|
+
field.width = 15
|
50
|
+
end
|
51
|
+
|
52
|
+
# Method B: specifying field name outside the block
|
53
|
+
flat_file.add_field :test2 do |field|
|
54
|
+
field.width = 15
|
55
|
+
end
|
56
|
+
|
57
|
+
field = flat_file.fields.last
|
58
|
+
expect( field.name ).to eq( :test2 )
|
59
|
+
|
60
|
+
expect( flat_file.fields.size ).to eq( 2)
|
61
|
+
expect( flat_file.width ).to eq( 30 )
|
62
|
+
expect( flat_file.pack_format ).to eq( 'A15A15' )
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe 'pad' do
|
67
|
+
before do
|
68
|
+
flat_file.reset_file_data
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'adds a named pad field to flat file' do
|
72
|
+
field = flat_file.pad :test, width: 12
|
73
|
+
expect( field.padding? ).to be true
|
74
|
+
expect( field.name ).to eq( :test )
|
75
|
+
expect( flat_file.fields.size ).to eq( 1 )
|
76
|
+
expect( flat_file.width ).to eq( 12 )
|
77
|
+
expect( flat_file.pack_format ).to eq( 'A12' )
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'adds an auto named pad field to flat file' do
|
81
|
+
field = flat_file.pad :autoname, width: 3
|
82
|
+
expect( field.padding? ).to be true
|
83
|
+
expect( field.name ).to eq( :pad_1 )
|
84
|
+
expect( flat_file.fields.size ).to eq( 1 )
|
85
|
+
expect( flat_file.width ).to eq( 3 )
|
86
|
+
expect( flat_file.pack_format ).to eq( 'A3' )
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe 'filter' do
|
91
|
+
before do
|
92
|
+
flat_file.reset_file_data
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'should not filter when none specified' do
|
96
|
+
field = flat_file.add_field :test
|
97
|
+
value = '123 '
|
98
|
+
filtered_value = field.filter value
|
99
|
+
expect( filtered_value ).to eq( '123 ' )
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'should filter for a specified block' do
|
103
|
+
field = flat_file.add_field :test, filter: ->(v) { v.strip }
|
104
|
+
value = '123 '
|
105
|
+
filtered_value = field.filter value
|
106
|
+
expect( filtered_value ).to eq( '123' )
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'should filter for a Filter Class' do
|
110
|
+
field = flat_file.add_field :test, filter: TestFilter
|
111
|
+
value = 'test'
|
112
|
+
filtered_value = field.filter value
|
113
|
+
expect( filtered_value ).to eq( 'TEST' )
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'should filter for an instance of a Filter Class' do
|
117
|
+
test_filter = TestFilter.new
|
118
|
+
field = flat_file.add_field :test, filter: test_filter
|
119
|
+
value = 'AbCd'
|
120
|
+
filtered_value = field.filter value
|
121
|
+
expect( filtered_value ).to eq( 'dCbA' )
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe FileData do
|
4
|
+
|
5
|
+
let(:flat_file) { Flat::File }
|
6
|
+
|
7
|
+
describe 'flat_file_data' do
|
8
|
+
it 'should be a Hash' do
|
9
|
+
expect( flat_file.flat_file_data ).to be_an_instance_of( Hash )
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'should have 4 keys' do
|
13
|
+
keys = flat_file.flat_file_data.keys
|
14
|
+
expect( keys.size ).to eq( 4 )
|
15
|
+
expect( keys ).to include( :width )
|
16
|
+
expect( keys ).to include( :pack_format )
|
17
|
+
expect( keys ).to include( :fields )
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe 'width' do
|
22
|
+
before do
|
23
|
+
flat_file.reset_file_data
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'has a convenience accessor' do
|
27
|
+
expect( flat_file.flat_file_data[:width] ).to eq( flat_file.width )
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'defaults to 0' do
|
31
|
+
expect( flat_file.width ).to eq( 0 )
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'can be changed' do
|
35
|
+
flat_file.width += 1
|
36
|
+
expect( flat_file.width ).to eq( 1 )
|
37
|
+
|
38
|
+
flat_file.width -= 2
|
39
|
+
expect( flat_file.width ).to eq( -1 )
|
40
|
+
|
41
|
+
flat_file.width = 12
|
42
|
+
expect( flat_file.width ).to eq( 12 )
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Record do
|
4
|
+
|
5
|
+
let(:flat_file) { Flat::File }
|
6
|
+
|
7
|
+
describe Record::Definition do
|
8
|
+
before do
|
9
|
+
flat_file.reset_file_data
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'creates a new instance' do
|
13
|
+
record = Record::Definition.new flat_file, {}, 12
|
14
|
+
expect( record.line_number ).to eq( 12 )
|
15
|
+
expect( record.attributes ).to be_empty
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'has a getter per defined field' do
|
19
|
+
flat_file.add_field :field, width: 25
|
20
|
+
record = Record::Definition.new flat_file, {field: 'Field'}
|
21
|
+
expect( record.field ).to eq( 'Field' )
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'has a setter per defined field' do
|
25
|
+
flat_file.add_field :field, width: 25
|
26
|
+
record = Record::Definition.new flat_file, {field: 'Field'}
|
27
|
+
record.field = record.field.upcase
|
28
|
+
expect( record.field ).to eq( 'FIELD' )
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'throws an error for an unknown attribute' do
|
32
|
+
skip 'not capturing raised error correctly'
|
33
|
+
|
34
|
+
record = Record::Definition.new flat_file, {}, 12
|
35
|
+
expect( record.field ).to raise_error( Errors::FlatFileError )
|
36
|
+
end
|
37
|
+
|
38
|
+
end # => describe Record::Definition
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'flat_file_helper'
|
3
|
+
|
4
|
+
describe Flat do
|
5
|
+
|
6
|
+
describe 'Version' do
|
7
|
+
it 'should verify current gem version' do
|
8
|
+
expect(Flat::VERSION).to eq('0.1.0')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe 'Operations' do
|
13
|
+
let(:person_file) { PersonFile.new }
|
14
|
+
let(:data) { PersonFile::EXAMPLE_FILE }
|
15
|
+
let(:lines) { data.split("\n") }
|
16
|
+
let(:stream) { StringIO.new(data) }
|
17
|
+
|
18
|
+
it 'reads data from a flat file' do
|
19
|
+
count = 0
|
20
|
+
person_file.each_record( stream ) do |x, y|
|
21
|
+
count += 1
|
22
|
+
end
|
23
|
+
expect( count ).to eq( lines.size )
|
24
|
+
end
|
25
|
+
|
26
|
+
end # => describe 'Operations'
|
27
|
+
|
28
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# Setup the RSpec testing environment for Flat:
|
2
|
+
#
|
3
|
+
require 'coveralls'
|
4
|
+
Coveralls.wear!
|
5
|
+
|
6
|
+
require 'pry'
|
7
|
+
require 'flat'
|
8
|
+
|
9
|
+
class TestFilter
|
10
|
+
def self.filter(value)
|
11
|
+
value.upcase
|
12
|
+
end
|
13
|
+
|
14
|
+
def filter(value)
|
15
|
+
value.reverse
|
16
|
+
end
|
17
|
+
end
|