flat 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,3 @@
1
+ module Flat #:nodoc:
2
+ VERSION = "0.1.0"
3
+ end
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,5 @@
1
+ require 'spec_helper'
2
+
3
+ describe Flat::File do
4
+
5
+ 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
@@ -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