ach_builder 0.0.2 → 0.2.1

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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +2 -1
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +10 -0
  7. data/Gemfile +11 -2
  8. data/MIT-LICENSE +1 -1
  9. data/README.md +19 -11
  10. data/Rakefile +44 -0
  11. data/ach_builder.gemspec +9 -5
  12. data/lib/ach/batch.rb +10 -32
  13. data/lib/ach/batch/builder.rb +60 -0
  14. data/lib/ach/batch/control.rb +18 -3
  15. data/lib/ach/batch/header.rb +21 -4
  16. data/lib/ach/component.rb +125 -43
  17. data/lib/ach/component/has_many_association.rb +155 -0
  18. data/lib/ach/constants.rb +15 -1
  19. data/lib/ach/file.rb +50 -44
  20. data/lib/ach/file/builder.rb +81 -0
  21. data/lib/ach/file/control.rb +15 -3
  22. data/lib/ach/file/header.rb +20 -3
  23. data/lib/ach/file/reader.rb +103 -0
  24. data/lib/ach/file/transmission_header.rb +77 -0
  25. data/lib/ach/formatter.rb +113 -76
  26. data/lib/ach/formatter/rule.rb +27 -0
  27. data/lib/ach/record.rb +13 -64
  28. data/lib/ach/record/addenda.rb +25 -0
  29. data/lib/ach/record/base.rb +109 -0
  30. data/lib/ach/record/dynamic.rb +58 -0
  31. data/lib/ach/record/entry.rb +49 -0
  32. data/lib/ach/record/tail.rb +10 -0
  33. data/lib/ach/validations.rb +5 -3
  34. data/lib/ach/version.rb +1 -1
  35. data/lib/ach_builder.rb +20 -33
  36. data/spec/batch_spec.rb +23 -11
  37. data/spec/componenet/has_many_association_spec.rb +111 -0
  38. data/spec/file_spec.rb +173 -81
  39. data/spec/formatter_spec.rb +9 -9
  40. data/spec/reader_spec.rb +55 -0
  41. data/spec/record/addenda_spec.rb +8 -0
  42. data/spec/record/base_spec.rb +53 -0
  43. data/spec/record/dynamic_spec.rb +74 -0
  44. data/spec/record/entry_spec.rb +7 -0
  45. data/spec/record/tail_spec.rb +7 -0
  46. data/spec/spec_helper.rb +1 -0
  47. data/spec/support/ach_file_factory.rb +70 -0
  48. data/spec/support/examples/well_fargo_empty.ach +2 -0
  49. data/spec/support/examples/well_fargo_with_data.ach +6 -0
  50. data/spec/support/helpers/ach_files_examples.rb +17 -0
  51. metadata +69 -26
  52. data/lib/ach/entry.rb +0 -31
  53. data/lib/ach/tail.rb +0 -6
  54. data/spec/entry_spec.rb +0 -7
  55. data/spec/record_spec.rb +0 -34
  56. data/spec/tail_spec.rb +0 -7
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a6370b31599af5467812e5b45be57e99c5e19a32
4
+ data.tar.gz: 326db2ed6596c13a95ad0deeb8b5a9881270e857
5
+ SHA512:
6
+ metadata.gz: a039dc192619a516cdf9c09fdefe33dde1505655c6b67c9bdacfd7ab47301a5586b1d1b5cd01292ef244cbd1fece3aa81b7344a9c4168f40e0a68d6694ac31f5
7
+ data.tar.gz: 5175200c43de130adb641b6d6c3d247b860b6e186cf2ff9ecc871dae6d95f0465f189a04f9d0e8f2430c811b77c74ad6272bbdea8b8109f6d667037aafce5fff
data/.gitignore CHANGED
@@ -2,3 +2,15 @@
2
2
  .bundle
3
3
  Gemfile.lock
4
4
  pkg/*
5
+ /.yardoc
6
+ .idea
7
+ doc/
8
+
9
+ # exclude everything in tmp
10
+ tmp/*
11
+ # except the metric_fu directory
12
+ !tmp/metric_fu/
13
+ # but exclude everything *in* the metric_fu directory
14
+ tmp/metric_fu/*
15
+ # except for the _data directory to track metrical outputs
16
+ !tmp/metric_fu/_data/
data/.rspec CHANGED
@@ -1,2 +1,3 @@
1
1
  --color
2
- --format documentation
2
+ --format documentation
3
+ --profile
@@ -0,0 +1 @@
1
+ ach_builder
@@ -0,0 +1 @@
1
+ 2.0.0-p247
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ script: "rake spec"
3
+ rvm:
4
+ - 1.9.3
5
+ - 2.0.0
6
+ notifications:
7
+ email:
8
+ - rubygems@tmxcredit.com
9
+ - AKuzko@sphereconsultinginc.com
10
+ - SPotapov@sphereconsultinginc.com
data/Gemfile CHANGED
@@ -1,5 +1,14 @@
1
- source "http://rubygems.org"
1
+ source "https://rubygems.org"
2
+
2
3
 
3
4
  # Specify your gem's dependencies in ach_builder.gemspec
5
+ gem 'i18n'
4
6
  gemspec
5
- gem 'rspec'
7
+ gem 'rspec'
8
+
9
+ group :development, :test do
10
+ gem 'rspec'
11
+
12
+ gem "simplecov" , :require => false
13
+ gem 'simplecov-rcov-text', :require => false
14
+ end
@@ -1,4 +1,4 @@
1
- Copyright (c) 2011 Artem Kuzko
1
+ Copyright (c) 2013 TMX Credit
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,41 +1,49 @@
1
1
  #ACH-Builder
2
2
 
3
- Ruby helper for creating ACH files. It's API is designed to be as flexible as possible.
3
+ [![Build Status](https://secure.travis-ci.org/TMXCredit/ach_builder.png)](http://travis-ci.org/TMXCredit/ach_builder)
4
4
 
5
- Inspired by
5
+ Ruby helper for creating ACH files. Its API is designed to be as flexible as possible.
6
6
 
7
- [ach](http://github.com/jm81/ach) - Ruby helper
8
-
9
- [ACH::Builder](http://github.com/camerb/ACH-Builder) - Perl module
10
-
11
- with similar functionality
7
+ Inspired by:
8
+ * [ach](http://github.com/jm81/ach) - Ruby helper
9
+ * [ACH::Builder](http://github.com/camerb/ACH-Builder) - Perl module
12
10
 
13
11
  ##Example
12
+
13
+ ```ruby
14
14
  # attributes for records may be passed as parameters, as well as modified in block
15
15
  # these attributes will be passed to all inner entities in a cascade way, if required
16
16
  file = ACH::File.new(:company_id => '11-11111', :company_name => 'MY COMPANY') do
17
17
  immediate_dest_name 'COMMERCE BANK'
18
18
  immediate_origin '123123123'
19
- immediate_oreigin_name 'MYCOMPANY'
20
-
19
+ immediate_origin_name 'MYCOMPANY'
20
+
21
21
  ['WEB', 'TEL'].each do |code|
22
22
  batch(:entry_class_code => code, :company_entry_descr => 'TV-TELCOM') do
23
23
  effective_date Time.now.strftime('%y%m%d')
24
24
  origin_dfi_id "00000000"
25
+
25
26
  entry :customer_name => 'JOHN SMITH',
26
27
  :customer_acct => '61242882282',
27
28
  :amount => '2501',
28
29
  :routing_number => '010010101',
29
30
  :bank_account => '103030030'
31
+ addenda do
32
+ addenda_type_code '05'
33
+ payment_related_info 'foo bar'
34
+ addenda_sequence_num 1
35
+ entry_details_sequence_num 1
36
+ end
30
37
  end
31
38
  end
32
39
  end
33
-
40
+
34
41
  file.valid? # => false
35
42
  file.errors # => {"ACH::File::Header#1"=>{:immediate_dest=>"is required"}}
36
43
  file.header.immediate_dest = '123123123'
37
44
  file.write('ach_01.txt')
45
+ ```
38
46
 
39
47
  ##Copyright
40
48
 
41
- Copyright (c) 2011 Artem Kuzko, released under the MIT license
49
+ Copyright (c) 2013 TMX Credit, released under the MIT license
data/Rakefile CHANGED
@@ -1 +1,45 @@
1
+ task :default => :spec
2
+
1
3
  require "bundler/gem_tasks"
4
+
5
+ require 'rspec/core/rake_task'
6
+ RSpec::Core::RakeTask.new(:spec) do |spec|
7
+ spec.pattern = FileList['spec/**/*_spec.rb']
8
+ end
9
+
10
+ def gemspec
11
+ @gem_spec ||= eval( open( `ls *.gemspec`.strip ){|file| file.read } )
12
+ end
13
+
14
+ def gem_version
15
+ gemspec.version
16
+ end
17
+
18
+ def gem_version_tag
19
+ "v#{gem_version}"
20
+ end
21
+
22
+ def gem_name
23
+ gemspec.name
24
+ end
25
+
26
+ def gem_file_name
27
+ "#{gem_name}-#{gem_version}.gem"
28
+ end
29
+
30
+ namespace :git do
31
+ desc "Create git version tag #{gem_version}"
32
+ task :tag do
33
+ sh "git tag -a #{gem_version_tag} -m \"Version #{gem_version}\""
34
+ end
35
+
36
+ desc "Push git tag to GitHub"
37
+ task :push_tags do
38
+ sh 'git push --tags'
39
+ end
40
+
41
+ desc "Create git version tag #{gem_version} and push to GitHub"
42
+ task :submit => [:tag, :push_tags] do
43
+ puts "Deployed to GitHub."
44
+ end
45
+ end
@@ -5,9 +5,9 @@ require "ach/version"
5
5
  Gem::Specification.new do |s|
6
6
  s.name = "ach_builder"
7
7
  s.version = ACH::VERSION
8
- s.authors = ["Artem Kuzko"]
9
- s.email = ["a.kuzko@gmail.com"]
10
- s.homepage = "http://github.com/akuzko/ach_builder"
8
+ s.authors = ["TMX Credit", "Artem Kuzko", "Sergey Potapov"]
9
+ s.email = ["rubygems@tmxcredit.com", "AKuzko@sphereconsultinginc.com", "SPotapov@sphereconsultinginc.com"]
10
+ s.homepage = "https://github.com/TMXCredit/ach_builder"
11
11
  s.summary = "Ruby tools for building ACH files"
12
12
  s.description = "Ruby tools for building ACH (Automated Clearing House) files"
13
13
 
@@ -17,7 +17,11 @@ Gem::Specification.new do |s|
17
17
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
18
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
19
  s.require_paths = ["lib"]
20
+ s.licenses = ['MIT']
20
21
 
21
- s.add_development_dependency "rspec", ">= 2.0.0"
22
- s.add_runtime_dependency "active_support", ">= 2.3.0"
22
+ s.add_development_dependency "rspec" , ">= 2.0.0"
23
+ s.add_runtime_dependency "activesupport", ">= 2.3.0"
24
+
25
+ # Necessary for active_support/inflector:
26
+ s.add_runtime_dependency "i18n"
23
27
  end
@@ -1,37 +1,15 @@
1
1
  module ACH
2
+ # Represents an ACH::Component, which is located under ACH::File and
3
+ # contains a variable number of ACH::Record::Entry and ACH::Record::Addenda
4
+ # records itself.
2
5
  class Batch < Component
6
+ autoload :Builder
7
+ autoload :Control
8
+ autoload :Header
9
+
10
+ include Builder
11
+
3
12
  has_many :entries
4
-
5
- def has_credit?
6
- entries.any?(&:credit?)
7
- end
8
-
9
- def has_debit?
10
- entries.any?(&:debit?)
11
- end
12
-
13
- def entry_count
14
- entries.length
15
- end
16
-
17
- def entry_hash
18
- entries.map{ |e| e.routing_number.to_i / 10 }.compact.inject(&:+)
19
- end
20
-
21
- def total_debit_amount
22
- entries.select(&:debit?).map{ |e| e.amount.to_i }.compact.inject(&:+) || 0
23
- end
24
-
25
- def total_credit_amount
26
- entries.select(&:credit?).map{ |e| e.amount.to_i }.compact.inject(&:+) || 0
27
- end
28
-
29
- def to_ach
30
- [header] + entries + [control]
31
- end
32
-
33
- def before_header
34
- attributes[:service_class_code] ||= (has_debit? && has_credit? ? 200 : has_debit? ? 225 : 220)
35
- end
13
+ has_many :addendas, :linked_to => :entries
36
14
  end
37
15
  end
@@ -0,0 +1,60 @@
1
+ module ACH
2
+ # Supports building a string representation of a particular instance of an
3
+ # ACH batch. Supports building ACH lines. Included by ACH::File.
4
+ module Batch::Builder
5
+ # Returns +true+ if any of internal ACH entries has 'credit' transaction code
6
+ def has_credit?
7
+ entries.any?(&:credit?)
8
+ end
9
+
10
+ # Returns +true+ if any internal ACH entry has 'debit' transaction code
11
+ def has_debit?
12
+ entries.any?(&:debit?)
13
+ end
14
+
15
+ # Returns total amount of entry and addenda records within batch
16
+ def entry_addenda_count
17
+ entries.size + addendas.values.flatten.size
18
+ end
19
+
20
+ # Returns 'hashed' representation of all entries within batch. See NACHA
21
+ # documentation for more details on entry hash
22
+ def entry_hash
23
+ entries.map{ |entry| entry.routing_number.to_i / 10 }.compact.inject(&:+)
24
+ end
25
+
26
+ # Returns total amount of all 'debit' entries within a batch
27
+ def total_debit_amount
28
+ amount_sum_for(:debit?)
29
+ end
30
+
31
+ # Returns total amount of all 'credit' entries within a batch
32
+ def total_credit_amount
33
+ amount_sum_for(:credit?)
34
+ end
35
+
36
+ # Returns ACH record objects that represent the batch
37
+ def to_ach
38
+ [header] + fetch_entries + [control]
39
+ end
40
+
41
+ # Helper method executed just before building a header record for the batch
42
+ def before_header
43
+ attributes[:service_class_code] ||= (has_debit? && has_credit? ? 200 : has_debit? ? 225 : 220)
44
+ end
45
+ private :before_header
46
+
47
+ # Helper method, returns total amount of all entries within a batch, filtered by +meth+
48
+ def amount_sum_for(meth)
49
+ entries.select(&meth).map{ |entry| entry.amount.to_i }.compact.inject(&:+) || 0
50
+ end
51
+ private :amount_sum_for
52
+
53
+ # Fetches all internal records (entries and addendas) in right order, i.e. addenda records
54
+ # should be positioned right after corresponding entry records.
55
+ def fetch_entries
56
+ entries.inject([]){ |all, entry| all << entry << addendas[entry] }.flatten.compact
57
+ end
58
+ private :fetch_entries
59
+ end
60
+ end
@@ -1,8 +1,23 @@
1
1
  module ACH
2
- class Batch::Control < Record
2
+ # Every ACH::Batch component ends with a batch control record.
3
+ #
4
+ # == Fields
5
+ #
6
+ # * record_type
7
+ # * service_class_code
8
+ # * entry_addenda_count
9
+ # * entry_hash
10
+ # * total_debit_amount
11
+ # * total_credit_amount
12
+ # * company_id
13
+ # * authen_code
14
+ # * bank_6
15
+ # * origin_dfi_id
16
+ # * batch_number
17
+ class Batch::Control < Record::Base
3
18
  fields :record_type,
4
19
  :service_class_code,
5
- :entry_count,
20
+ :entry_addenda_count,
6
21
  :entry_hash,
7
22
  :total_debit_amount,
8
23
  :total_credit_amount,
@@ -12,7 +27,7 @@ module ACH
12
27
  :origin_dfi_id,
13
28
  :batch_number
14
29
 
15
- defaults :record_type => 8,
30
+ defaults :record_type => BATCH_CONTROL_RECORD_TYPE,
16
31
  :authen_code => '',
17
32
  :bank_6 => ''
18
33
  end
@@ -1,5 +1,22 @@
1
1
  module ACH
2
- class Batch::Header < Record
2
+ # Every ACH::Batch component starts with a batch header record
3
+ #
4
+ # == Fields
5
+ #
6
+ # * record_type
7
+ # * service_class_code
8
+ # * company_name
9
+ # * company_note_data
10
+ # * company_id
11
+ # * entry_class_code
12
+ # * company_entry_descr
13
+ # * desc_date
14
+ # * effective_date
15
+ # * settlement_date
16
+ # * origin_status_code
17
+ # * origin_dfi_id
18
+ # * batch_number
19
+ class Batch::Header < Record::Base
3
20
  fields :record_type,
4
21
  :service_class_code,
5
22
  :company_name,
@@ -7,18 +24,18 @@ module ACH
7
24
  :company_id,
8
25
  :entry_class_code,
9
26
  :company_entry_descr,
10
- :date,
27
+ :desc_date,
11
28
  :effective_date,
12
29
  :settlement_date,
13
30
  :origin_status_code,
14
31
  :origin_dfi_id,
15
32
  :batch_number
16
33
 
17
- defaults :record_type => 5,
34
+ defaults :record_type => BATCH_HEADER_RECORD_TYPE,
18
35
  :service_class_code => 200,
19
36
  :company_note_data => '',
20
37
  :date => lambda{ Time.now.strftime("%y%m%d") },
21
38
  :settlement_date => '',
22
- :origin_status_code => ''
39
+ :origin_status_code => 1
23
40
  end
24
41
  end
@@ -1,39 +1,98 @@
1
1
  module ACH
2
+ # Base class for ACH::File and ACH::Batch. Every component has its own number
3
+ # of entities, header and control records. So it provides ACH::Component#header,
4
+ # ACH::Component#control, ACH::Component.has_many methods to manage them.
5
+ #
6
+ # == Example
7
+ #
8
+ # class File < Component
9
+ # has_many :batches
10
+ # # implementation
11
+ # end
2
12
  class Component
13
+ extend ActiveSupport::Autoload
14
+
3
15
  include Validations
4
16
  include Constants
5
-
6
- class UnknownAttribute < ArgumentError
7
- def initialize field
8
- super "Unrecognized attribute '#{field}'"
17
+
18
+ autoload :HasManyAssociation
19
+
20
+ # Exception raised on attempt to assign a value to nonexistent field.
21
+ class UnknownAttributeError < ArgumentError
22
+ def initialize field, obj
23
+ super "Unrecognized attribute '#{field}' for #{obj}"
9
24
  end
10
25
  end
11
26
 
27
+ class_attribute :default_attributes
28
+ class_attribute :after_initialize_hooks
29
+ self.default_attributes = {}
30
+ self.after_initialize_hooks = []
31
+
12
32
  attr_reader :attributes
13
-
14
- def initialize fields = {}, &block
15
- @attributes = {}
33
+
34
+ def self.inherited(klass)
35
+ klass.default_attributes = default_attributes.dup
36
+ klass.after_initialize_hooks = after_initialize_hooks.dup
37
+ end
38
+
39
+ # Uses +method_missing+ pattern to specify default attributes for a
40
+ # +Component+. If method name is one of the defined rules, saves it to
41
+ # +default_attributes+ hash.
42
+ #
43
+ # These attributes are passed to inner components in a cascade way, i.e. when ACH
44
+ # File was defined with default value for 'company_name', this value will be passed
45
+ # to every Batch component within file, and from every Batch to corresponding batch
46
+ # header record.
47
+ #
48
+ # Note that default values may be overwritten when building records.
49
+ def self.method_missing(meth, *args)
50
+ if Formatter.defined?(meth)
51
+ default_attributes[meth] = args.first
52
+ else
53
+ super
54
+ end
55
+ end
56
+
57
+ def initialize(fields = {}, &block)
58
+ @attributes = {}.merge(self.class.default_attributes)
16
59
  fields.each do |name, value|
17
- raise UnknownAttribute.new(name) unless Formatter::RULES.key?(name)
60
+ raise UnknownAttributeError.new(name, self) unless Formatter.defined?(name)
18
61
  @attributes[name] = value
19
62
  end
20
- after_initialize if respond_to?(:after_initialize)
63
+ after_initialize
21
64
  instance_eval(&block) if block
22
65
  end
23
-
24
- def method_missing meth, *args
25
- if Formatter::RULES.key?(meth)
66
+
67
+ def method_missing(meth, *args)
68
+ if Formatter.defined?(meth)
26
69
  args.empty? ? @attributes[meth] : (@attributes[meth] = args.first)
27
70
  else
28
71
  super
29
72
  end
30
73
  end
31
-
32
- def before_header
74
+
75
+ def before_header # :nodoc:
33
76
  end
34
77
  private :before_header
35
-
36
- def header fields = {}, &block
78
+
79
+ # Sets header fields if fields or block passed. Returns header record.
80
+ #
81
+ # == Example 1
82
+ #
83
+ # header :foo => "value 1", :bar => "value 2"
84
+ #
85
+ # == Example 2
86
+ #
87
+ # header do
88
+ # foo "value 1"
89
+ # bar "value 2"
90
+ # end
91
+ #
92
+ # == Example 3
93
+ #
94
+ # header # => just returns a header object
95
+ def header(fields = {}, &block)
37
96
  before_header
38
97
  merged_fields = fields_for(self.class::Header).merge(fields)
39
98
  @header ||= self.class::Header.new(merged_fields)
@@ -41,39 +100,62 @@ module ACH
41
100
  head.instance_eval(&block) if block
42
101
  end
43
102
  end
44
-
103
+
104
+ def build_header(str) # :nodoc:
105
+ @header = self.class::Header.from_s(str)
106
+ end
107
+
45
108
  def control
46
- klass = self.class::Control
47
- fields = klass.fields.select{ |f| respond_to?(f) || attributes[f] }
48
- klass.new Hash[*fields.zip(fields.map{ |f| send(f) }).flatten]
109
+ @control ||= begin
110
+ klass = self.class::Control
111
+ fields = klass.fields.select{ |f| respond_to?(f) || attributes[f] }
112
+ klass.new Hash[*fields.zip(fields.map{ |f| send(f) }).flatten]
113
+ end
49
114
  end
50
-
51
- def fields_for component_or_class
52
- klass = component_or_class.is_a?(Class) ? component_or_class : "ACH::#{component_or_class.camelize}".constantize
53
- klass < Component ? attributes : attributes.select{ |k, v| klass.fields.include?(k) && attributes[k] }
115
+
116
+ def build_control(str) # :nodoc:
117
+ @control = self.class::Control.from_s(str)
54
118
  end
55
119
 
56
- def self.has_many plural_name, proc_defaults = nil
57
- attr_reader plural_name
58
-
59
- singular_name = plural_name.to_s.singularize
60
- klass = "ACH::#{singular_name.camelize}".constantize
61
-
62
- define_method(singular_name) do |*args, &block|
63
- index_or_fields = args.first || {}
64
- return send(plural_name)[index_or_fields] if Fixnum === index_or_fields
65
-
66
- defaults = proc_defaults ? instance_exec(&proc_defaults) : {}
67
-
68
- klass.new(fields_for(singular_name).merge(defaults).merge(index_or_fields)).tap do |component|
69
- component.instance_eval(&block) if block
70
- send(plural_name) << component
71
- end
120
+ def fields_for(klass)
121
+ if klass < Component
122
+ attributes
123
+ else
124
+ attrs = attributes.find_all{ |k, v| klass.fields.include?(k) && attributes[k] }
125
+ Hash[*attrs.flatten]
72
126
  end
73
-
74
- define_method :after_initialize do
75
- instance_variable_set("@#{plural_name}", [])
127
+ end
128
+
129
+ def after_initialize # :nodoc:
130
+ self.class.after_initialize_hooks.each{ |hook| instance_exec(&hook) }
131
+ end
132
+
133
+ # Creates has many association.
134
+ #
135
+ # == Example
136
+ #
137
+ # class File < Component
138
+ # has_many :batches
139
+ # end
140
+ #
141
+ # file = File.new do
142
+ # batch :foo => 1, :bar => 2
143
+ # end
144
+ #
145
+ # file.batches # => [#<Batch ...>]
146
+ #
147
+ # The example above extends File with #batches and #batch instance methods:
148
+ # * #batch is used to add new instance of Batch.
149
+ # * #batches is used to get an array of batches which belong to file.
150
+ def self.has_many(plural_name, options = {})
151
+ association = HasManyAssociation.new(plural_name, options)
152
+
153
+ association_variable_name = "@#{plural_name}_association"
154
+ association.delegation_methods.each do |method_name|
155
+ delegate method_name, :to => association_variable_name
76
156
  end
157
+
158
+ after_initialize_hooks << lambda{ instance_variable_set(association_variable_name, association.for(self)) }
77
159
  end
78
160
  end
79
161
  end