lib/ach/component.rb in ach_builder-0.0.2 vs lib/ach/component.rb in ach_builder-0.2.1
- old
+ new
@@ -1,79 +1,161 @@
module ACH
+ # Base class for ACH::File and ACH::Batch. Every component has its own number
+ # of entities, header and control records. So it provides ACH::Component#header,
+ # ACH::Component#control, ACH::Component.has_many methods to manage them.
+ #
+ # == Example
+ #
+ # class File < Component
+ # has_many :batches
+ # # implementation
+ # end
class Component
+ extend ActiveSupport::Autoload
+
include Validations
include Constants
-
- class UnknownAttribute < ArgumentError
- def initialize field
- super "Unrecognized attribute '#{field}'"
+
+ autoload :HasManyAssociation
+
+ # Exception raised on attempt to assign a value to nonexistent field.
+ class UnknownAttributeError < ArgumentError
+ def initialize field, obj
+ super "Unrecognized attribute '#{field}' for #{obj}"
end
end
+ class_attribute :default_attributes
+ class_attribute :after_initialize_hooks
+ self.default_attributes = {}
+ self.after_initialize_hooks = []
+
attr_reader :attributes
-
- def initialize fields = {}, &block
- @attributes = {}
+
+ def self.inherited(klass)
+ klass.default_attributes = default_attributes.dup
+ klass.after_initialize_hooks = after_initialize_hooks.dup
+ end
+
+ # Uses +method_missing+ pattern to specify default attributes for a
+ # +Component+. If method name is one of the defined rules, saves it to
+ # +default_attributes+ hash.
+ #
+ # These attributes are passed to inner components in a cascade way, i.e. when ACH
+ # File was defined with default value for 'company_name', this value will be passed
+ # to every Batch component within file, and from every Batch to corresponding batch
+ # header record.
+ #
+ # Note that default values may be overwritten when building records.
+ def self.method_missing(meth, *args)
+ if Formatter.defined?(meth)
+ default_attributes[meth] = args.first
+ else
+ super
+ end
+ end
+
+ def initialize(fields = {}, &block)
+ @attributes = {}.merge(self.class.default_attributes)
fields.each do |name, value|
- raise UnknownAttribute.new(name) unless Formatter::RULES.key?(name)
+ raise UnknownAttributeError.new(name, self) unless Formatter.defined?(name)
@attributes[name] = value
end
- after_initialize if respond_to?(:after_initialize)
+ after_initialize
instance_eval(&block) if block
end
-
- def method_missing meth, *args
- if Formatter::RULES.key?(meth)
+
+ def method_missing(meth, *args)
+ if Formatter.defined?(meth)
args.empty? ? @attributes[meth] : (@attributes[meth] = args.first)
else
super
end
end
-
- def before_header
+
+ def before_header # :nodoc:
end
private :before_header
-
- def header fields = {}, &block
+
+ # Sets header fields if fields or block passed. Returns header record.
+ #
+ # == Example 1
+ #
+ # header :foo => "value 1", :bar => "value 2"
+ #
+ # == Example 2
+ #
+ # header do
+ # foo "value 1"
+ # bar "value 2"
+ # end
+ #
+ # == Example 3
+ #
+ # header # => just returns a header object
+ def header(fields = {}, &block)
before_header
merged_fields = fields_for(self.class::Header).merge(fields)
@header ||= self.class::Header.new(merged_fields)
@header.tap do |head|
head.instance_eval(&block) if block
end
end
-
+
+ def build_header(str) # :nodoc:
+ @header = self.class::Header.from_s(str)
+ end
+
def control
- klass = self.class::Control
- fields = klass.fields.select{ |f| respond_to?(f) || attributes[f] }
- klass.new Hash[*fields.zip(fields.map{ |f| send(f) }).flatten]
+ @control ||= begin
+ klass = self.class::Control
+ fields = klass.fields.select{ |f| respond_to?(f) || attributes[f] }
+ klass.new Hash[*fields.zip(fields.map{ |f| send(f) }).flatten]
+ end
end
+
+ def build_control(str) # :nodoc:
+ @control = self.class::Control.from_s(str)
+ end
- def fields_for component_or_class
- klass = component_or_class.is_a?(Class) ? component_or_class : "ACH::#{component_or_class.camelize}".constantize
- klass < Component ? attributes : attributes.select{ |k, v| klass.fields.include?(k) && attributes[k] }
+ def fields_for(klass)
+ if klass < Component
+ attributes
+ else
+ attrs = attributes.find_all{ |k, v| klass.fields.include?(k) && attributes[k] }
+ Hash[*attrs.flatten]
+ end
end
+
+ def after_initialize # :nodoc:
+ self.class.after_initialize_hooks.each{ |hook| instance_exec(&hook) }
+ end
- def self.has_many plural_name, proc_defaults = nil
- attr_reader plural_name
-
- singular_name = plural_name.to_s.singularize
- klass = "ACH::#{singular_name.camelize}".constantize
-
- define_method(singular_name) do |*args, &block|
- index_or_fields = args.first || {}
- return send(plural_name)[index_or_fields] if Fixnum === index_or_fields
-
- defaults = proc_defaults ? instance_exec(&proc_defaults) : {}
-
- klass.new(fields_for(singular_name).merge(defaults).merge(index_or_fields)).tap do |component|
- component.instance_eval(&block) if block
- send(plural_name) << component
- end
+ # Creates has many association.
+ #
+ # == Example
+ #
+ # class File < Component
+ # has_many :batches
+ # end
+ #
+ # file = File.new do
+ # batch :foo => 1, :bar => 2
+ # end
+ #
+ # file.batches # => [#<Batch ...>]
+ #
+ # The example above extends File with #batches and #batch instance methods:
+ # * #batch is used to add new instance of Batch.
+ # * #batches is used to get an array of batches which belong to file.
+ def self.has_many(plural_name, options = {})
+ association = HasManyAssociation.new(plural_name, options)
+
+ association_variable_name = "@#{plural_name}_association"
+ association.delegation_methods.each do |method_name|
+ delegate method_name, :to => association_variable_name
end
-
- define_method :after_initialize do
- instance_variable_set("@#{plural_name}", [])
- end
+
+ after_initialize_hooks << lambda{ instance_variable_set(association_variable_name, association.for(self)) }
end
end
end