gotime-slither 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c3d2ee3717c7e3ed3187c30b6f5913f15ca2d382
4
+ data.tar.gz: c941cd80a505c801172bf0377343b27064df2eb0
5
+ SHA512:
6
+ metadata.gz: 2ab17161a128a546574ea4f1cd2b5be6f39363005c7578a0eb758a381e0d2afa1a8eba1c0bf421d233e456dd9396cf43429fd7434bb52a4d8accffde40e4894e
7
+ data.tar.gz: 3b9c5d9746c2cb86b943efdcbeb0871f93de2b7509cbb5e86d04f1ca253acd3299dc277618e8d84c57d1f312dfc4657e1a5788aee0d79337200bb0238e87a292
@@ -0,0 +1,16 @@
1
+
2
+ == 0.99.2 / 2009-04-28
3
+
4
+ * Added better support for float formatting
5
+ * Added the money_with_implied_decimal type
6
+
7
+ == 0.99.1 / 2009-04-22
8
+
9
+ * Make the missing method build a column i.e. body.record_type 1
10
+ * Prevent duplicate column names
11
+ * Better error messages
12
+ * Implement custom padding (spaces (default), zero)
13
+
14
+ == 0.99.0 / 2009-04-14
15
+
16
+ * Initial Release
@@ -0,0 +1,109 @@
1
+ == slither
2
+ by Ryan Wood
3
+ http://ryanwood.com
4
+
5
+ == DESCRIPTION:
6
+
7
+ A simple, clean DSL for describing, writing, and parsing fixed-width text files.
8
+
9
+ == FEATURES:
10
+
11
+ * Easy DSL syntax
12
+ * Can parse and format fixed width files
13
+ * Templated sections for reuse
14
+ * Helpful error messages for invalid data
15
+
16
+ == SYNOPSIS:
17
+
18
+ # Create a Slither::Defintion to describe a file format
19
+ Slither.define :simple, :by_bytes => false do |d|
20
+
21
+ # This is a template section that can be reused in other sections
22
+ d.template :boundary do |t|
23
+ t.column :record_type, 4
24
+ t.column :company_id, 12
25
+ end
26
+
27
+ # Create a header section
28
+ d.header :align => :left do |header|
29
+ # The trap tells Slither which lines should fall into this section
30
+ header.trap { |line| line[0,4] == 'HEAD' }
31
+ # Use the boundary template for the columns
32
+ header.template :boundary
33
+ end
34
+
35
+ d.body do |body|
36
+ body.trap { |line| line[0,4] =~ /[^(HEAD|FOOT)]/ }
37
+ body.column :id, 10, :type => :integer
38
+ body.column :name, 10, :align => :left
39
+ body.spacer 3
40
+ body.column :state, 2
41
+ end
42
+
43
+ d.footer do |footer|
44
+ footer.trap { |line| line[0,4] == 'FOOT' }
45
+ footer.template :boundary
46
+ footer.column :record_count, 10
47
+ end
48
+ end
49
+
50
+ Supported types are: string, integer, date, float, binary, money, and money_with_implied_decimal.
51
+
52
+ Use :by_bytes => true (default) to allow newlines within rows and specify length in bytes.
53
+ Use :by_bytes => false to support sections of different lengths and length specification
54
+ in number of characters.
55
+
56
+ Binary types will be returned as an array of 8-bit unsigned byte values
57
+
58
+ Then either feed it a nested struct with data values to create the file in the defined format:
59
+
60
+ test_data = {
61
+ :body => [
62
+ { :id => 12, :name => "Ryan", :state => 'SC' },
63
+ { :id => 23, :name => "Joe", :state => 'VA' },
64
+ { :id => 42, :name => "Tommy", :state => 'FL' },
65
+ ],
66
+ :header => { :record_type => 'HEAD', :company_id => 'ABC' },
67
+ :footer => { :record_type => 'FOOT', :company_id => 'ABC' }
68
+ }
69
+
70
+ # Generates the file as a string
71
+ puts Slither.generate(:simple, test_data)
72
+
73
+ # Writes the file
74
+ Slither.write(output_filename, :simple, test_data)
75
+
76
+ or parse files already in that format into a nested hash:
77
+
78
+ parsed_data = Slither.parse(input_filename, :simple)
79
+ parsed_data = Slither.parseIo(io_object, :simple)
80
+
81
+
82
+ == INSTALL:
83
+
84
+ sudo gem install slither
85
+
86
+ == LICENSE:
87
+
88
+ (The MIT License)
89
+
90
+ Copyright (c) 2008
91
+
92
+ Permission is hereby granted, free of charge, to any person obtaining
93
+ a copy of this software and associated documentation files (the
94
+ 'Software'), to deal in the Software without restriction, including
95
+ without limitation the rights to use, copy, modify, merge, publish,
96
+ distribute, sublicense, and/or sell copies of the Software, and to
97
+ permit persons to whom the Software is furnished to do so, subject to
98
+ the following conditions:
99
+
100
+ The above copyright notice and this permission notice shall be
101
+ included in all copies or substantial portions of the Software.
102
+
103
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
104
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
105
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
106
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
107
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
108
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
109
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,37 @@
1
+ require 'rake'
2
+ require 'spec/rake/spectask'
3
+
4
+ desc "Run all examples with RCov"
5
+ Spec::Rake::SpecTask.new('rcov') do |t|
6
+ t.spec_files = FileList['spec/*.rb']
7
+ t.rcov = true
8
+ t.rcov_opts = ['--exclude', 'spec']
9
+ end
10
+
11
+ begin
12
+ require 'bones'
13
+ Bones.setup
14
+ rescue LoadError
15
+ load 'tasks/setup.rb'
16
+ end
17
+
18
+ ensure_in_path 'lib'
19
+ require 'bones'
20
+
21
+ task :default => 'spec:run'
22
+
23
+ PROJ.name = 'slither'
24
+ PROJ.authors = 'Ryan Wood'
25
+ PROJ.email = 'ryan.wood@gmail.com'
26
+ PROJ.url = 'http://github.com/ryanwood/slither'
27
+ PROJ.version = '0.99.3'
28
+ PROJ.exclude = %w(\.git .gitignore ^tasks \.eprj ^pkg)
29
+ PROJ.readme_file = 'README.rdoc'
30
+
31
+ #PROJ.rubyforge.name = 'codeforpeople'
32
+
33
+ PROJ.rdoc.exclude << '^data'
34
+ PROJ.notes.exclude = %w(^README\.rdoc$ ^data ^pkg)
35
+
36
+ # PROJ.svn.path = 'bones'
37
+ # PROJ.spec.opts << '--color'
data/TODO ADDED
@@ -0,0 +1,14 @@
1
+ == 0.99.2
2
+
3
+ * Add :limit option on sections
4
+ * Add :validation option for columns
5
+ * Add a validate_file() method to parse a file and run all validation tests (implies validation implemented)
6
+
7
+ == 1.0.0
8
+
9
+ * Better Documentation
10
+
11
+ == 1.x
12
+
13
+ * Alternate Section Flow (other than linear), i.e. repeatable sections (think batch)
14
+ * Equivalent of parse by bytes, but with chars. Use "io.gets nil, num_chars_per_line"
Binary file
@@ -0,0 +1,7 @@
1
+ $: << File.dirname(__FILE__)
2
+ require 'slither/slither'
3
+ require 'slither/definition'
4
+ require 'slither/section'
5
+ require 'slither/column'
6
+ require 'slither/parser'
7
+ require 'slither/generator'
@@ -0,0 +1,146 @@
1
+ require 'date'
2
+
3
+ class Slither
4
+ class ParserError < RuntimeError; end
5
+
6
+ class Column
7
+ attr_reader :name, :length, :alignment, :type, :padding, :precision, :options
8
+
9
+ def initialize(name, length, options = {})
10
+ assert_valid_options(options)
11
+ @name = name
12
+ @length = length
13
+ @options = options
14
+ @alignment = options[:align] || :right
15
+ @type = options[:type] || :string
16
+ @padding = options[:padding] || :space
17
+ @truncate = options[:truncate] || false
18
+ # Only used with floats, this determines the decimal places
19
+ @precision = options[:precision]
20
+ end
21
+
22
+ def unpacker
23
+ case @type
24
+ when :binary
25
+ "C#{@length}"
26
+ else
27
+ "A#{@length}"
28
+ end
29
+ end
30
+
31
+ def parse_length
32
+ case @type
33
+ when :binary
34
+ @length
35
+ else
36
+ 1
37
+ end
38
+ end
39
+
40
+ def parse(value)
41
+ case @type
42
+ when :integer
43
+ value.to_i
44
+ when :float, :money
45
+ value.to_f
46
+ when :money_with_implied_decimal
47
+ value.to_f / 100
48
+ when :binary
49
+ value
50
+ when :date
51
+ if @options[:format]
52
+ Date.strptime(value, @options[:format])
53
+ else
54
+ Date.strptime(value)
55
+ end
56
+ else value.strip
57
+ end
58
+ rescue
59
+ raise ParserError, "Error parsing column ''#{name}'. The value '#{value}' could not be converted to type #{@type}: #{$!}"
60
+ end
61
+
62
+ def format(value)
63
+ pad(formatter % to_s(value))
64
+ rescue
65
+ puts "Could not format column '#{@name}' as a '#{@type}' with formatter '#{formatter}' and value of '#{value}' (formatted: '#{to_s(value)}'). #{$!}"
66
+ end
67
+
68
+ private
69
+
70
+ def formatter
71
+ "%#{aligner}#{sizer}s"
72
+ end
73
+
74
+ def aligner
75
+ @alignment == :left ? '-' : ''
76
+ end
77
+
78
+ def sizer
79
+ (@type == :float && @precision) ? @precision : @length
80
+ end
81
+
82
+ # Manually apply padding. sprintf only allows padding on numeric fields.
83
+ def pad(value)
84
+ return value unless @padding == :zero
85
+ matcher = @alignment == :right ? /^ +/ : / +$/
86
+ space = value.match(matcher)
87
+ return value unless space
88
+ value.gsub(space[0], '0' * space[0].size)
89
+ end
90
+
91
+ def inspect
92
+ "#<#{self.class} #{instance_variables.map{|iv| "#{iv}=>#{instance_variable_get(iv)}"}.join(', ')}>"
93
+ end
94
+
95
+ def to_s(value)
96
+ result = case @type
97
+ when :date
98
+ # If it's a DBI::Timestamp object, see if we can convert it to a Time object
99
+ unless value.respond_to?(:strftime)
100
+ value = value.to_time if value.respond_to?(:to_time)
101
+ end
102
+ if value.respond_to?(:strftime)
103
+ if @options[:format]
104
+ value.strftime(@options[:format])
105
+ else
106
+ value.strftime
107
+ end
108
+ else
109
+ value.to_s
110
+ end
111
+ when :float
112
+ @options[:format] ? @options[:format] % value.to_f : value.to_f.to_s
113
+ when :money
114
+ "%.2f" % value.to_f
115
+ when :money_with_implied_decimal
116
+ "%d" % (value.to_f * 100)
117
+ else
118
+ value.to_s
119
+ end
120
+ validate_size result
121
+ end
122
+
123
+ def assert_valid_options(options)
124
+ unless options[:align].nil? || [:left, :right].include?(options[:align])
125
+ raise ArgumentError, "Option :align only accepts :right (default) or :left"
126
+ end
127
+ unless options[:padding].nil? || [:space, :zero].include?(options[:padding])
128
+ raise ArgumentError, "Option :padding only accepts :space (default) or :zero"
129
+ end
130
+ end
131
+
132
+ def validate_size(result)
133
+ # Handle when length is out of range
134
+ if result.length > @length
135
+ if @truncate
136
+ start = @alignment == :left ? 0 : -@length
137
+ result = result[start, @length]
138
+ else
139
+ raise Slither::FormattedStringExceedsLengthError,
140
+ "The formatted value '#{result}' in column '#{@name}' exceeds the allowed length of #{@length} chararacters."
141
+ end
142
+ end
143
+ result
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,35 @@
1
+ class Slither
2
+ class Definition
3
+ attr_reader :sections, :templates, :options
4
+
5
+ def initialize(options = {})
6
+ @sections = []
7
+ @templates = {}
8
+ @options = { :align => :right, :by_bytes => true, :validate_length => true,
9
+ :error_handler => nil, force_character_offset: false, :newline_style => :unix,
10
+ :terminal_newline => false }.merge(options)
11
+ end
12
+
13
+ def section(name, options = {}, &block)
14
+ raise( ArgumentError, "Reserved or duplicate section name: '#{name}'") if
15
+ Section::RESERVED_NAMES.include?( name ) ||
16
+ (@sections.size > 0 && @sections.map{ |s| s.name }.include?( name ))
17
+
18
+ section = Slither::Section.new(name, @options.merge(options))
19
+ section.definition = self
20
+ yield(section)
21
+ @sections << section
22
+ section
23
+ end
24
+
25
+ def template(name, options = {}, &block)
26
+ section = Slither::Section.new(name, @options.merge(options))
27
+ yield(section)
28
+ @templates[name] = section
29
+ end
30
+
31
+ def method_missing(method, *args, &block)
32
+ section(method, *args, &block)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,37 @@
1
+ class Slither
2
+ class Generator
3
+
4
+ def initialize(definition)
5
+ @definition = definition
6
+ end
7
+
8
+ def generate(data)
9
+ @builder = []
10
+ @definition.sections.each do |section|
11
+ content = data[section.name]
12
+ if content
13
+ content = [content] unless content.is_a?(Array)
14
+ raise(Slither::RequiredSectionEmptyError, "Required section '#{section.name}' was empty.") if content.empty?
15
+ content.each do |row|
16
+ @builder << section.format(row)
17
+ end
18
+ else
19
+ raise(Slither::RequiredSectionEmptyError, "Required section '#{section.name}' was empty.") unless section.optional
20
+ end
21
+ end
22
+ newline_style = newline_lookup(@definition.options[:newline_style])
23
+
24
+ output_string = @builder.join(newline_style)
25
+ output_string << newline_style if @definition.options[:terminal_newline]
26
+
27
+ output_string
28
+ end
29
+
30
+ private
31
+
32
+ def newline_lookup(option)
33
+ option == :dos ? "\r\n" : "\n"
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,109 @@
1
+ class Slither
2
+ class Parser
3
+
4
+ def initialize(definition, file_io)
5
+ @definition = definition
6
+ @file = file_io
7
+ # This may be used in the future for non-linear or repeating sections
8
+ @mode = :linear
9
+ end
10
+
11
+ def parse(error_handler=nil)
12
+ parsed = {}
13
+
14
+ @file.each_line do |line|
15
+ line.chomp! if line
16
+ next if line.empty?
17
+ @definition.sections.each do |section|
18
+ if section.match(line)
19
+ validate_length(line, section, error_handler) if @definition.options[:validate_length]
20
+ parsed = fill_content(line, section, parsed)
21
+ end
22
+ end
23
+ end
24
+
25
+ @definition.sections.each do |section|
26
+ raise(Slither::RequiredSectionNotFoundError, "Required section '#{section.name}' was not found.") unless parsed[section.name] || section.optional
27
+ end
28
+ parsed
29
+ end
30
+
31
+ def parse_by_bytes
32
+ parsed = {}
33
+
34
+ all_section_lengths = @definition.sections.map{|sec| sec.length }
35
+ byte_length = all_section_lengths.max
36
+ all_section_lengths.each { |bytes| raise(Slither::SectionsNotSameLengthError,
37
+ "All sections must have the same number of bytes for parse by bytes") if bytes != byte_length }
38
+
39
+ while record = @file.read(byte_length)
40
+
41
+ unless remove_newlines! && byte_length == record.length
42
+ parsed_line = parse_for_error_message(record)
43
+ raise(Slither::LineWrongSizeError, "Line wrong size: No newline at #{byte_length} bytes. #{parsed_line}")
44
+ end
45
+
46
+ record.force_encoding @file.external_encoding
47
+
48
+ @definition.sections.each do |section|
49
+ if section.match(record)
50
+ parsed = fill_content(record, section, parsed)
51
+ end
52
+ end
53
+ end
54
+
55
+ @definition.sections.each do |section|
56
+ raise(Slither::RequiredSectionNotFoundError, "Required section '#{section.name}' was not found.") unless parsed[section.name] || section.optional
57
+ end
58
+ parsed
59
+ end
60
+
61
+ private
62
+
63
+ def fill_content(line, section, parsed)
64
+ parsed[section.name] ||= []
65
+ parsed[section.name] << section.parse(line)
66
+ parsed
67
+ end
68
+
69
+ def validate_length(line, section, error_handler)
70
+ if line.length != section.length
71
+ if error_handler
72
+ error_handler.call(line)
73
+ else
74
+ parsed_line = parse_for_error_message(line)
75
+ raise Slither::LineWrongSizeError, "Line wrong size: (#{line.length} when it should be #{section.length}. #{parsed_line})"
76
+ end
77
+ end
78
+ end
79
+
80
+ def remove_newlines!
81
+ return true if @file.eof?
82
+ b = @file.getbyte
83
+ if b == 10 || b == 13 && @file.getbyte == 10
84
+ return true
85
+ else
86
+ @file.ungetbyte b
87
+ return false
88
+ end
89
+ end
90
+
91
+ def newline?(char_code)
92
+ # \n or LF -> 10
93
+ # \r or CR -> 13
94
+ [10, 13].any?{|code| char_code == code}
95
+ end
96
+
97
+ def parse_for_error_message(line)
98
+ parsed = ''
99
+ line.force_encoding @file.external_encoding
100
+ @definition.sections.each do |section|
101
+ if section.match(line)
102
+ parsed = section.parse_when_problem(line)
103
+ end
104
+ end
105
+ parsed
106
+ end
107
+
108
+ end
109
+ end