flat 0.1.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.
- 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
|