Class: Fixy::Record

Inherits:
Object
  • Object
show all
Defined in:
lib/fixy/record.rb

Constant Summary collapse

LINE_ENDING_LF =
"\n".freeze
LINE_ENDING_CR =
"\r".freeze
LINE_ENDING_CRLF =
"#{LINE_ENDING_CR}#{LINE_ENDING_LF}".freeze
DEFAULT_LINE_ENDING =
LINE_ENDING_LF

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.default_record_fieldsObject



72
73
74
75
76
77
78
# File 'lib/fixy/record.rb', line 72

def default_record_fields
  if superclass.respond_to?(:record_fields, true) && superclass.record_fields
    superclass.record_fields.dup
  else
    {}
  end
end

.field(name, size, range, type, &block) ⇒ Object

Raises:

  • (ArgumentError)


17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/fixy/record.rb', line 17

def field(name, size, range, type, &block)
  @record_fields ||= default_record_fields
  range_matches = range.match /^(\d+)(?:-(\d+))?$/

  # Make sure inputs are valid, we rather fail early than behave unexpectedly later.
  raise ArgumentError, "Name '#{name}' is not a symbol"  unless name.is_a? Symbol
  raise ArgumentError, "Size '#{size}' is not a numeric" unless size.is_a?(Numeric) && size > 0
  raise ArgumentError, "Range '#{range}' is invalid"     unless range_matches
  raise ArgumentError, "Unknown type '#{type}'"          unless (private_instance_methods + instance_methods).include? "format_#{type}".to_sym

  # Validate the range is consistent with size
  range_from  = Integer(range_matches[1])
  range_to    = Integer(range_matches[2].nil? ? range_matches[1] : range_matches[2])
  valid_range = (range_from + (size - 1) == range_to)

  raise ArgumentError, "Invalid Range (size: #{size}, range: #{range})" unless valid_range
  raise ArgumentError, "Invalid Range (> #{record_length})"             unless range_to <= record_length

  # Ensure range is not already covered by another definition
  (1..range_to).each do |column|
    if @record_fields[column] && @record_fields[column][:to] >= range_from
      raise ArgumentError, "Column #{column} has already been allocated"
    end
  end

  # We're good to go :)
  @record_fields[range_from] = { name: name, from: range_from, to: range_to, size: size, type: type}

  field_value(name, block) if block_given?
end

.field_value(name, value) ⇒ Object

Convenience method for creating field methods



49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/fixy/record.rb', line 49

def field_value(name, value)

  # Make sure we're not overriding an existing method
  if (private_instance_methods + instance_methods).include?(name)
    raise ArgumentError, "Method '#{name}' is already defined, watch out for conflicts."
  end

  if value.is_a? Proc
    define_method(name) { self.instance_exec(&value) }
  else
    define_method(name) { value }
  end
end

.line_endingObject



67
68
69
70
# File 'lib/fixy/record.rb', line 67

def line_ending
  # Use the default line ending unless otherwise specified
  @line_ending || DEFAULT_LINE_ENDING
end

.parse(record, debug = false) ⇒ Object

Parse an existing record

Raises:

  • (ArgumentError)


81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/fixy/record.rb', line 81

def parse(record, debug = false)
  raise ArgumentError, 'Record must be a string'  unless record.is_a? String

  unless record.bytesize == record_length
    raise ArgumentError, "Record length is invalid (Expected #{record_length})"
  end

  decorator = debug ? Fixy::Decorator::Debug : Fixy::Decorator::Default
  fields = []
  output = ''
  current_position = 1
  current_record = 1

  byte_record = record.bytes.to_a
  while current_position <= record_length do

    field = record_fields[current_position]
    raise StandardError, "Undefined field for position #{current_position}" unless field

    # Extract field data from existing record
    from   = field[:from] - 1
    to     = field[:to]   - 1
    method = field[:name]
    value  = byte_record[from..to].pack('C*').force_encoding('utf-8')

    formatted_value = decorator.field(value, current_record, current_position, method, field[:size], field[:type])
    output << formatted_value
    fields << { name:  method, value: value }

    current_position = field[:to] + 1
    current_record += 1
  end

  # Documentation mandates that every record ends with new line.
  output << line_ending

  { fields: fields, record: decorator.record(output) }
end

.record_fieldsObject



63
64
65
# File 'lib/fixy/record.rb', line 63

def record_fields
  @record_fields
end

.set_line_ending(character) ⇒ Object



13
14
15
# File 'lib/fixy/record.rb', line 13

def set_line_ending(character)
  @line_ending = character
end

.set_record_length(count) ⇒ Object



9
10
11
# File 'lib/fixy/record.rb', line 9

def set_record_length(count)
  define_singleton_method('record_length') { count }
end

Instance Method Details

#generate(debug = false) ⇒ Object

Generate the entry based on the record structure



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/fixy/record.rb', line 122

def generate(debug = false)
  decorator = debug ? Fixy::Decorator::Debug : Fixy::Decorator::Default
  output = ''
  current_position = 1
  current_record = 1

  while current_position <= self.class.record_length do

    field = record_fields[current_position]
    raise StandardError, "Undefined field for position #{current_position}" unless field

    # We will first retrieve the value, then format it
    method          = field[:name]
    value           = send(method)
    formatted_value = format_value(value, field[:size], field[:type])
    formatted_value = decorator.field(formatted_value, current_record, current_position, method, field[:size], field[:type])

    output << formatted_value
    current_position = field[:to] + 1
    current_record += 1
  end

  # Documentation mandates that every record ends with new line.
  output << line_ending

  # All ready. In the words of Mr. Peters: "Take it and go!"
  decorator.record(output)
end