Class: Fixy::Record

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

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.default_record_fieldsObject



58
59
60
61
62
63
64
# File 'lib/fixy/record.rb', line 58

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) ⇒ Object

Raises:

  • (ArgumentError)


8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/fixy/record.rb', line 8

def field(name, size, range, type)
  @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, Proc.new) if block_given?
end

.field_value(name, value) ⇒ Object

Convenience method for creating field methods



40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/fixy/record.rb', line 40

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

.parse(record, debug = false) ⇒ Object

Parse an existing record

Raises:

  • (ArgumentError)


67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/fixy/record.rb', line 67

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 << "\n"

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

.record_fieldsObject



54
55
56
# File 'lib/fixy/record.rb', line 54

def record_fields
  @record_fields
end

.set_record_length(count) ⇒ Object



4
5
6
# File 'lib/fixy/record.rb', line 4

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



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/fixy/record.rb', line 108

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 << "\n"

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