fencer 0.4.2
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.
- data/.rspec +1 -0
- data/LICENSE +22 -0
- data/README.md +66 -0
- data/Rakefile +2 -0
- data/fencer.gemspec +22 -0
- data/lib/fencer.rb +89 -0
- data/lib/fencer/version.rb +3 -0
- data/spec/fencer_spec.rb +142 -0
- metadata +83 -0
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color --format documentation
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Dan Cheail
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# Fencer
|
2
|
+
Fencer is designed to process rows of fixed-length and delimited text
|
3
|
+
data, splitting at designated termination points, converting field values
|
4
|
+
where required and making fields available through named object accessors.
|
5
|
+
|
6
|
+
Row formats are defined by subclassing Fencer::Base and using the DSL
|
7
|
+
provided.
|
8
|
+
|
9
|
+
## Example
|
10
|
+
class EmployeeRecord < Fencer::Base
|
11
|
+
field :department, 50, :string
|
12
|
+
field :name, 20, -> s { s.split }
|
13
|
+
space 2
|
14
|
+
field :age, 4, :integer
|
15
|
+
end
|
16
|
+
|
17
|
+
`field` takes 3 arguments: a field name, the field length and an (optional)
|
18
|
+
converter.
|
19
|
+
|
20
|
+
## Field Conversion
|
21
|
+
`Fencer::Base::Converters` is a `Hash<` defines some commonly-used converters.
|
22
|
+
It's left un-frozen, so it can be extended as required.
|
23
|
+
|
24
|
+
Short-cut methods for the default field types are also available:
|
25
|
+
|
26
|
+
class EmployeeRecord < Fencer::Base
|
27
|
+
string :department, 20 => String
|
28
|
+
integer :age, 2 => Integer
|
29
|
+
decimal :salary, 10 => BigDecimal
|
30
|
+
end
|
31
|
+
|
32
|
+
Additionally, custom conversions can be defined by passing a `lambda`
|
33
|
+
as the final argument.
|
34
|
+
|
35
|
+
## Usage
|
36
|
+
|
37
|
+
Records are extracted on initialisation:
|
38
|
+
|
39
|
+
raw_string = "EXAMPLE FORMAT 10 300.04"
|
40
|
+
fields = EmployeeRecord.new(raw_string)
|
41
|
+
|
42
|
+
And are directly accessible thereafter:
|
43
|
+
|
44
|
+
fields.department # => "EXAMPLE FORMAT"
|
45
|
+
fields.age # => 2
|
46
|
+
fields.salary # => BigDecimal("300.04")
|
47
|
+
|
48
|
+
In the case of importing delimiter-separated data, passing the delimiting
|
49
|
+
character as the second argument to `new` will yield the desired result
|
50
|
+
without any change of layout:
|
51
|
+
|
52
|
+
raw_string = "EXAMPLE FORMAT|10|300.04"
|
53
|
+
fields = EmployeeRecord.new(raw_string, "|")
|
54
|
+
|
55
|
+
fields.department # => "EXAMPLE FORMAT"
|
56
|
+
fields.age # => 2
|
57
|
+
fields.salary # => BigDecimal("300.04")
|
58
|
+
|
59
|
+
|
60
|
+
## Known Deficiencies
|
61
|
+
|
62
|
+
Currently, Fencer works with Ruby 1.9 only. Sorry. I wanted Hashes that
|
63
|
+
preserve field-order. P;us, the newer syntax is pretty.
|
64
|
+
|
65
|
+
Fencer is also blissfully unaware of any sort of encoding. This is a planned
|
66
|
+
feature for the 1.0 release.
|
data/Rakefile
ADDED
data/fencer.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/fencer/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Dan Cheail"]
|
6
|
+
gem.email = ["dan@undumb.com"]
|
7
|
+
gem.description = %q{Fixed-length/delimited record parser DSL}
|
8
|
+
gem.summary = %q{Fencer makes working with fixed-length and delimited
|
9
|
+
text-based records simpler by providing a flexible DSL
|
10
|
+
for defining field lengths and transformations}
|
11
|
+
|
12
|
+
gem.homepage = "https://github.com/undumb/fencer"
|
13
|
+
gem.files = `git ls-files`.split($\) - %w(Gemfile .gitignore)
|
14
|
+
# gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
15
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
16
|
+
gem.name = "fencer"
|
17
|
+
gem.require_paths = ["lib"]
|
18
|
+
gem.version = Fencer::VERSION
|
19
|
+
|
20
|
+
gem.add_development_dependency "rspec", "~> 2.10"
|
21
|
+
gem.add_development_dependency "rake"
|
22
|
+
end
|
data/lib/fencer.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require "bigdecimal"
|
2
|
+
require "fencer/version"
|
3
|
+
|
4
|
+
module Fencer
|
5
|
+
|
6
|
+
class Base
|
7
|
+
Converters = {
|
8
|
+
string: -> s { s.strip },
|
9
|
+
integer: -> s { s.to_i },
|
10
|
+
decimal: -> s { BigDecimal(s) },
|
11
|
+
}
|
12
|
+
|
13
|
+
class << self
|
14
|
+
attr_reader :fields
|
15
|
+
|
16
|
+
def inherited(subclass)
|
17
|
+
subclass.instance_variable_set(:@fields, {})
|
18
|
+
end
|
19
|
+
|
20
|
+
def field(name, size, convert = nil)
|
21
|
+
# error handling, ahoy!
|
22
|
+
raise "#{name} already defined as a field on #{self.name}" if fields.has_key?(name)
|
23
|
+
|
24
|
+
unless convert.nil? || Converters.has_key?(convert) || convert.is_a?(Proc)
|
25
|
+
raise "Invalid converter"
|
26
|
+
end
|
27
|
+
|
28
|
+
fields[name] = { size: size, convert: convert }
|
29
|
+
|
30
|
+
# create our attr method
|
31
|
+
define_method(name) { @values[name] }
|
32
|
+
end
|
33
|
+
|
34
|
+
def space(size)
|
35
|
+
fields[:"_#{fields.length.succ}"] = { size: size, space: true }
|
36
|
+
end
|
37
|
+
|
38
|
+
def string(name, size)
|
39
|
+
field(name, size, :string)
|
40
|
+
end
|
41
|
+
|
42
|
+
def integer(name, size)
|
43
|
+
field(name, size, :integer)
|
44
|
+
end
|
45
|
+
|
46
|
+
def decimal(name, size)
|
47
|
+
field(name, size, :decimal)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize(str, delimiter = nil)
|
52
|
+
@values = {}
|
53
|
+
@delimiter = delimiter
|
54
|
+
@str = str
|
55
|
+
|
56
|
+
parse!
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_hash
|
60
|
+
@values
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def parse!
|
66
|
+
if @delimiter
|
67
|
+
raw_values = @str.split(@delimiter)
|
68
|
+
else
|
69
|
+
unpack_phrase = self.class.fields.values.map { |s| "A#{s[:size]}" }.join
|
70
|
+
raw_values = @str.unpack(unpack_phrase)
|
71
|
+
end
|
72
|
+
|
73
|
+
_index = 0
|
74
|
+
self.class.fields.each do |name, opts|
|
75
|
+
unless opts[:space]
|
76
|
+
_conversion_proc = case opts[:convert]
|
77
|
+
when Symbol then Converters[opts[:convert]]
|
78
|
+
when Proc then opts[:convert]
|
79
|
+
else nil
|
80
|
+
end
|
81
|
+
|
82
|
+
@values[name] = _conversion_proc ? _conversion_proc.call(raw_values[_index]) : raw_values[_index]
|
83
|
+
end
|
84
|
+
|
85
|
+
_index += 1 unless opts[:space] && @delimiter
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/spec/fencer_spec.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
# require 'rubygems'
|
2
|
+
|
3
|
+
Bundler.require(:default, :development)
|
4
|
+
|
5
|
+
require 'rspec'
|
6
|
+
require 'fencer'
|
7
|
+
|
8
|
+
describe Fencer do
|
9
|
+
|
10
|
+
class EmployeeRecord < Fencer::Base
|
11
|
+
string :name, 20
|
12
|
+
string :department, 20
|
13
|
+
space 2
|
14
|
+
field :employment_date, 8, -> s { dasherise_ymd(s) }
|
15
|
+
integer :id_number, 6
|
16
|
+
decimal :leave_accrued, 11
|
17
|
+
|
18
|
+
class << self
|
19
|
+
def dasherise_ymd(str)
|
20
|
+
str.gsub(/([0-9]{4})([0-9]{2})([0-9]{2})/) { "#{$1}-#{$2}-#{$3}" }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
let(:name) { "Ford Prefect" }
|
26
|
+
let(:department) { "Publication" }
|
27
|
+
let(:employment_date) { "20120525" }
|
28
|
+
let(:id_number) { "42" }
|
29
|
+
let(:leave_accrued) { "1484.33" }
|
30
|
+
|
31
|
+
it "has many wonderful features" do
|
32
|
+
|
33
|
+
compiled_record = name.ljust(20)
|
34
|
+
compiled_record << department.ljust(20)
|
35
|
+
compiled_record << " "
|
36
|
+
compiled_record << employment_date
|
37
|
+
compiled_record << id_number.rjust(6, '0')
|
38
|
+
compiled_record << leave_accrued.rjust(11, '0')
|
39
|
+
|
40
|
+
# auto parsing
|
41
|
+
values = EmployeeRecord.new(compiled_record)
|
42
|
+
|
43
|
+
# individual accessors for each field
|
44
|
+
values.name.should eq(name)
|
45
|
+
values.department.should eq(department)
|
46
|
+
values.employment_date.should eq("2012-05-25")
|
47
|
+
values.id_number.should eq(id_number.to_i)
|
48
|
+
values.leave_accrued.should eq(BigDecimal(leave_accrued))
|
49
|
+
|
50
|
+
# export our values to a hash
|
51
|
+
values.to_hash.should eq({
|
52
|
+
name: name,
|
53
|
+
department: department,
|
54
|
+
employment_date: "2012-05-25",
|
55
|
+
id_number: id_number.to_i,
|
56
|
+
leave_accrued: BigDecimal(leave_accrued),
|
57
|
+
})
|
58
|
+
end
|
59
|
+
|
60
|
+
it "also works when parsing arbitrarily delimited fields!" do
|
61
|
+
compiled_record = [
|
62
|
+
name, department, employment_date, id_number, leave_accrued
|
63
|
+
].join("|")
|
64
|
+
|
65
|
+
values = EmployeeRecord.new(compiled_record, "|")
|
66
|
+
|
67
|
+
values.name.should eq(name)
|
68
|
+
values.department.should eq(department)
|
69
|
+
values.employment_date.should eq("2012-05-25")
|
70
|
+
values.id_number.should eq(id_number.to_i)
|
71
|
+
values.leave_accrued.should eq(BigDecimal(leave_accrued))
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
class FencerBaseTest < Fencer::Base
|
77
|
+
class << self
|
78
|
+
def reset_fields!
|
79
|
+
@fields = {}
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe Fencer::Base do
|
85
|
+
before(:each) do
|
86
|
+
subject.reset_fields!
|
87
|
+
end
|
88
|
+
|
89
|
+
subject { FencerBaseTest }
|
90
|
+
|
91
|
+
it "#field adds fields to the internal register" do
|
92
|
+
subject.field(:derp, 1)
|
93
|
+
subject.fields.should have_key(:derp)
|
94
|
+
end
|
95
|
+
|
96
|
+
it "#field adds an instance access method" do
|
97
|
+
subject.field(:derp, 1)
|
98
|
+
|
99
|
+
instance = subject.new("")
|
100
|
+
instance.should respond_to(:derp)
|
101
|
+
instance.should_not respond_to(:herp)
|
102
|
+
end
|
103
|
+
|
104
|
+
it "raises an error when the size attribute is omitted" do
|
105
|
+
expect { subject.field(:derp) }.to raise_error
|
106
|
+
end
|
107
|
+
|
108
|
+
it "raises an error when duplicate keys are defined" do
|
109
|
+
subject.field(:derp, 1)
|
110
|
+
expect { subject.field(:derp, 1) }.to raise_error
|
111
|
+
end
|
112
|
+
|
113
|
+
context "when using shortcut methods" do
|
114
|
+
it "has a shortcut for string fields" do
|
115
|
+
subject.string(:derp, 1)
|
116
|
+
subject.fields.should have_key(:derp)
|
117
|
+
subject.fields[:derp][:convert].should be :string
|
118
|
+
end
|
119
|
+
|
120
|
+
it "has a shortcut for integer fields" do
|
121
|
+
subject.integer(:derp, 1)
|
122
|
+
subject.fields.should have_key(:derp)
|
123
|
+
subject.fields[:derp][:convert].should be :integer
|
124
|
+
end
|
125
|
+
|
126
|
+
it "has a shortcut for decimal fields" do
|
127
|
+
subject.decimal(:derp, 1)
|
128
|
+
subject.fields.should have_key(:derp)
|
129
|
+
subject.fields[:derp][:convert].should be :decimal
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
context "when setting an invalid conversion argument" do
|
134
|
+
it "raises an error if the symbol is not registered" do
|
135
|
+
expect { subject.field(:derp, 1, :to_date) }.to raise_error
|
136
|
+
end
|
137
|
+
|
138
|
+
it "raises an error if the argument is not a lambda" do
|
139
|
+
expect { subject.field(:derp, 1, "") }.to raise_error
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
metadata
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fencer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Dan Cheail
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-05-31 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &70270773693060 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '2.10'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70270773693060
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rake
|
27
|
+
requirement: &70270773692560 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70270773692560
|
36
|
+
description: Fixed-length/delimited record parser DSL
|
37
|
+
email:
|
38
|
+
- dan@undumb.com
|
39
|
+
executables: []
|
40
|
+
extensions: []
|
41
|
+
extra_rdoc_files: []
|
42
|
+
files:
|
43
|
+
- .rspec
|
44
|
+
- LICENSE
|
45
|
+
- README.md
|
46
|
+
- Rakefile
|
47
|
+
- fencer.gemspec
|
48
|
+
- lib/fencer.rb
|
49
|
+
- lib/fencer/version.rb
|
50
|
+
- spec/fencer_spec.rb
|
51
|
+
homepage: https://github.com/undumb/fencer
|
52
|
+
licenses: []
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options: []
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
segments:
|
64
|
+
- 0
|
65
|
+
hash: 1981968692186319614
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ! '>='
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
segments:
|
73
|
+
- 0
|
74
|
+
hash: 1981968692186319614
|
75
|
+
requirements: []
|
76
|
+
rubyforge_project:
|
77
|
+
rubygems_version: 1.8.17
|
78
|
+
signing_key:
|
79
|
+
specification_version: 3
|
80
|
+
summary: Fencer makes working with fixed-length and delimited text-based records simpler
|
81
|
+
by providing a flexible DSL for defining field lengths and transformations
|
82
|
+
test_files:
|
83
|
+
- spec/fencer_spec.rb
|