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.
@@ -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