activerecord 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activerecord might be problematic. Click here for more details.
- data/CHANGELOG +581 -0
- data/README +361 -0
- data/RUNNING_UNIT_TESTS +36 -0
- data/dev-utils/eval_debugger.rb +9 -0
- data/examples/associations.png +0 -0
- data/examples/associations.rb +87 -0
- data/examples/shared_setup.rb +15 -0
- data/examples/validation.rb +88 -0
- data/install.rb +60 -0
- data/lib/active_record.rb +48 -0
- data/lib/active_record/aggregations.rb +165 -0
- data/lib/active_record/associations.rb +536 -0
- data/lib/active_record/associations/association_collection.rb +70 -0
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +46 -0
- data/lib/active_record/associations/has_many_association.rb +104 -0
- data/lib/active_record/base.rb +985 -0
- data/lib/active_record/callbacks.rb +337 -0
- data/lib/active_record/connection_adapters/abstract_adapter.rb +326 -0
- data/lib/active_record/connection_adapters/mysql_adapter.rb +131 -0
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +177 -0
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +107 -0
- data/lib/active_record/deprecated_associations.rb +70 -0
- data/lib/active_record/fixtures.rb +172 -0
- data/lib/active_record/observer.rb +71 -0
- data/lib/active_record/reflection.rb +126 -0
- data/lib/active_record/support/class_attribute_accessors.rb +43 -0
- data/lib/active_record/support/class_inheritable_attributes.rb +37 -0
- data/lib/active_record/support/clean_logger.rb +10 -0
- data/lib/active_record/support/inflector.rb +70 -0
- data/lib/active_record/transactions.rb +102 -0
- data/lib/active_record/validations.rb +205 -0
- data/lib/active_record/vendor/mysql.rb +1117 -0
- data/lib/active_record/vendor/simple.rb +702 -0
- data/lib/active_record/wrappers/yaml_wrapper.rb +15 -0
- data/lib/active_record/wrappings.rb +59 -0
- data/rakefile +122 -0
- data/test/abstract_unit.rb +16 -0
- data/test/aggregations_test.rb +34 -0
- data/test/all.sh +8 -0
- data/test/associations_test.rb +477 -0
- data/test/base_test.rb +513 -0
- data/test/class_inheritable_attributes_test.rb +33 -0
- data/test/connections/native_mysql/connection.rb +24 -0
- data/test/connections/native_postgresql/connection.rb +24 -0
- data/test/connections/native_sqlite/connection.rb +24 -0
- data/test/deprecated_associations_test.rb +336 -0
- data/test/finder_test.rb +67 -0
- data/test/fixtures/accounts/signals37 +3 -0
- data/test/fixtures/accounts/unknown +2 -0
- data/test/fixtures/auto_id.rb +4 -0
- data/test/fixtures/column_name.rb +3 -0
- data/test/fixtures/companies/first_client +6 -0
- data/test/fixtures/companies/first_firm +4 -0
- data/test/fixtures/companies/second_client +6 -0
- data/test/fixtures/company.rb +37 -0
- data/test/fixtures/company_in_module.rb +33 -0
- data/test/fixtures/course.rb +3 -0
- data/test/fixtures/courses/java +2 -0
- data/test/fixtures/courses/ruby +2 -0
- data/test/fixtures/customer.rb +30 -0
- data/test/fixtures/customers/david +6 -0
- data/test/fixtures/db_definitions/mysql.sql +96 -0
- data/test/fixtures/db_definitions/mysql2.sql +4 -0
- data/test/fixtures/db_definitions/postgresql.sql +113 -0
- data/test/fixtures/db_definitions/postgresql2.sql +4 -0
- data/test/fixtures/db_definitions/sqlite.sql +85 -0
- data/test/fixtures/db_definitions/sqlite2.sql +4 -0
- data/test/fixtures/default.rb +2 -0
- data/test/fixtures/developer.rb +8 -0
- data/test/fixtures/developers/david +2 -0
- data/test/fixtures/developers/jamis +2 -0
- data/test/fixtures/developers_projects/david_action_controller +2 -0
- data/test/fixtures/developers_projects/david_active_record +2 -0
- data/test/fixtures/developers_projects/jamis_active_record +2 -0
- data/test/fixtures/entrant.rb +3 -0
- data/test/fixtures/entrants/first +3 -0
- data/test/fixtures/entrants/second +3 -0
- data/test/fixtures/entrants/third +3 -0
- data/test/fixtures/fixture_database.sqlite +0 -0
- data/test/fixtures/fixture_database_2.sqlite +0 -0
- data/test/fixtures/movie.rb +5 -0
- data/test/fixtures/movies/first +2 -0
- data/test/fixtures/movies/second +2 -0
- data/test/fixtures/project.rb +3 -0
- data/test/fixtures/projects/action_controller +2 -0
- data/test/fixtures/projects/active_record +2 -0
- data/test/fixtures/reply.rb +21 -0
- data/test/fixtures/subscriber.rb +5 -0
- data/test/fixtures/subscribers/first +2 -0
- data/test/fixtures/subscribers/second +2 -0
- data/test/fixtures/topic.rb +20 -0
- data/test/fixtures/topics/first +9 -0
- data/test/fixtures/topics/second +8 -0
- data/test/fixtures_test.rb +20 -0
- data/test/inflector_test.rb +104 -0
- data/test/inheritance_test.rb +125 -0
- data/test/lifecycle_test.rb +110 -0
- data/test/modules_test.rb +21 -0
- data/test/multiple_db_test.rb +46 -0
- data/test/pk_test.rb +57 -0
- data/test/reflection_test.rb +78 -0
- data/test/thread_safety_test.rb +33 -0
- data/test/transactions_test.rb +83 -0
- data/test/unconnected_test.rb +24 -0
- data/test/validations_test.rb +126 -0
- metadata +166 -0
@@ -0,0 +1,172 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
# Fixtures are a way of organizing data that you want to test against. Each fixture file is created as a row
|
4
|
+
# in the database and created as a hash with column names as keys and data as values. All of these fixture hashes
|
5
|
+
# are kept in an overall hash where they can be accessed by their file name.
|
6
|
+
#
|
7
|
+
# Example:
|
8
|
+
#
|
9
|
+
# Directory with the fixture files
|
10
|
+
#
|
11
|
+
# developers/
|
12
|
+
# david
|
13
|
+
# luke
|
14
|
+
# jamis
|
15
|
+
#
|
16
|
+
# The file +david+ then contains:
|
17
|
+
#
|
18
|
+
# id => 1
|
19
|
+
# name => David Heinemeier Hansson
|
20
|
+
# birthday => 1979-10-15
|
21
|
+
# profession => Systems development
|
22
|
+
#
|
23
|
+
# Now when we call <tt>@developers = Fixtures.new(ActiveRecord::Base.connection, "developers", "developers/")</tt> all three
|
24
|
+
# developers will get inserted into the "developers" table through the active Active Record connection (that must be setup
|
25
|
+
# before-hand). And we can now query the fixture data through the <tt>@developers</tt> hash, so <tt>@developers["david"]["name"]</tt>
|
26
|
+
# will return <tt>"David Heinemeier Hansson"</tt> and <tt>@developers["david"]["birthday"]</tt> will return <tt>Date.new(1979, 10, 15)</tt>.
|
27
|
+
#
|
28
|
+
# This can then be used for comparison in a unit test. Something like:
|
29
|
+
#
|
30
|
+
# def test_find
|
31
|
+
# assert_equal @developers["david"]["name"], Developer.find(@developers["david"]["id"]).name
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# == YAML fixtures
|
35
|
+
#
|
36
|
+
# Additionally, fixtures supports yaml files. Like fixture files, these yaml files have a pre-defined format. The document
|
37
|
+
# must be formatted like this:
|
38
|
+
#
|
39
|
+
# name: david
|
40
|
+
# data:
|
41
|
+
# id: 1
|
42
|
+
# name: David Heinemeier Hansson
|
43
|
+
# birthday: 1979-10-15
|
44
|
+
# profession: Systems development
|
45
|
+
# ---
|
46
|
+
# name: steve
|
47
|
+
# data:
|
48
|
+
# id: 2
|
49
|
+
# name: Steve Ross Kellock
|
50
|
+
# birthday: 1974-09-27
|
51
|
+
# profession: guy with keyboard
|
52
|
+
#
|
53
|
+
# In that file, there's two records. Each record must have two parts: 'name' and 'data'. The data that you add
|
54
|
+
# must be indented like you see above.
|
55
|
+
#
|
56
|
+
# Yaml fixtures file names must end with .yaml as in people.yaml or camel.yaml. The yaml fixtures are placed in the same
|
57
|
+
# directory as the normal fixtures and can happy co-exist. :)
|
58
|
+
class Fixtures
|
59
|
+
def self.create_fixtures(fixtures_directory, *table_names)
|
60
|
+
connection = block_given? ? yield : ActiveRecord::Base.connection
|
61
|
+
ActiveRecord::Base.logger.level = Logger::ERROR
|
62
|
+
|
63
|
+
fixtures = [ table_names ].flatten.collect do |table_name|
|
64
|
+
Fixtures.new(connection, table_name, "#{fixtures_directory}/#{table_name}")
|
65
|
+
end
|
66
|
+
|
67
|
+
ActiveRecord::Base.logger.level = Logger::DEBUG
|
68
|
+
|
69
|
+
return fixtures.size > 1 ? fixtures : fixtures.first
|
70
|
+
end
|
71
|
+
|
72
|
+
def initialize(connection, table_name, fixture_path, file_filter = /^\.|CVS|\.yaml/)
|
73
|
+
@connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
|
74
|
+
@fixtures = read_fixtures
|
75
|
+
|
76
|
+
delete_existing_fixtures
|
77
|
+
insert_fixtures
|
78
|
+
end
|
79
|
+
|
80
|
+
# Access a fixture hash by using its file name as the key
|
81
|
+
def [](key)
|
82
|
+
@fixtures[key]
|
83
|
+
end
|
84
|
+
|
85
|
+
# Get the number of fixtures kept in this container
|
86
|
+
def length
|
87
|
+
@fixtures.length
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
def read_fixtures
|
92
|
+
Dir.entries(@fixture_path).inject({}) do |fixtures, file|
|
93
|
+
# is this a regular fixture file?
|
94
|
+
fixtures[file] = Fixture.new(@fixture_path, file) unless file =~ @file_filter
|
95
|
+
# is this a *.yaml file?
|
96
|
+
if file =~ /\.yaml/
|
97
|
+
YamlFixture.produce( "#{@fixture_path}/#{file}" ).each { |fix| fixtures[fix.yaml_name] = fix }
|
98
|
+
end
|
99
|
+
fixtures
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def delete_existing_fixtures
|
104
|
+
@connection.delete "DELETE FROM #{@table_name}"
|
105
|
+
end
|
106
|
+
|
107
|
+
def insert_fixtures
|
108
|
+
@fixtures.values.each do |fixture|
|
109
|
+
@connection.execute "INSERT INTO #{@table_name} (#{fixture.key_list}) VALUES(#{fixture.value_list})"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def []=(key, value)
|
114
|
+
@fixtures[key] = value
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
class Fixture #:nodoc:
|
119
|
+
def initialize(fixture_path, file)
|
120
|
+
@fixture_path, @file = fixture_path, file
|
121
|
+
@fixture = read_fixture
|
122
|
+
end
|
123
|
+
|
124
|
+
def [](key)
|
125
|
+
@fixture[key]
|
126
|
+
end
|
127
|
+
|
128
|
+
def to_hash
|
129
|
+
@fixture
|
130
|
+
end
|
131
|
+
|
132
|
+
def key_list
|
133
|
+
@fixture.keys.join(", ")
|
134
|
+
end
|
135
|
+
|
136
|
+
def value_list
|
137
|
+
@fixture.values.map { |v| "'#{v}'" }.join(", ")
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
def read_fixture
|
142
|
+
IO.readlines("#{@fixture_path}/#{@file}").inject({}) do |fixture, line|
|
143
|
+
key, value = line.split(/ => /)
|
144
|
+
fixture[key.strip] = value.strip
|
145
|
+
fixture
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# A YamlFixture is like a fixture, but instead of a name to use as
|
151
|
+
# a key, it uses a yaml_name.
|
152
|
+
class YamlFixture < Fixture #:nodoc:
|
153
|
+
# yaml_name is equivalent to a normal fixture's filename
|
154
|
+
attr_accessor :yaml_name
|
155
|
+
|
156
|
+
# constructor is passed the name & the actual instantiate fixture
|
157
|
+
def initialize(yaml_name, fixture)
|
158
|
+
@yaml_name, @fixture = yaml_name, fixture
|
159
|
+
end
|
160
|
+
|
161
|
+
# given a valid yaml file name, create an array of YamlFixture objects
|
162
|
+
def self.produce( yaml_file_name )
|
163
|
+
results = []
|
164
|
+
yaml_file = File.open( yaml_file_name )
|
165
|
+
YAML::load_documents( yaml_file ) do |doc|
|
166
|
+
f = YamlFixture.new( doc['name'], doc['data'] )
|
167
|
+
results << f
|
168
|
+
end
|
169
|
+
yaml_file.close
|
170
|
+
results
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
# Observers can be programmed to react to lifecycle callbacks in another class to implement
|
5
|
+
# trigger-like behavior outside the original class. This is a great way to reduce the clutter that
|
6
|
+
# normally comes when the model class is burdened with excess responsibility that doesn't pertain to
|
7
|
+
# the core and nature of the class. Example:
|
8
|
+
#
|
9
|
+
# class CommentObserver < ActiveRecord::Observer
|
10
|
+
# def after_save(comment)
|
11
|
+
# NotificationServer.send_email("admin@do.com", "New comment was posted", comment)
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# This Observer is triggered when a Comment#save is finished and sends a notification about it to the administrator.
|
16
|
+
#
|
17
|
+
# == Observing a class that can't be infered
|
18
|
+
#
|
19
|
+
# Observers will by default be mapped to the class with which they share a name. So CommentObserver will
|
20
|
+
# be tied to observing Comment, ProductManagerObserver to ProductManager, and so on. If you want to name your observer
|
21
|
+
# something else than the class you're interested in observing, you can implement the observed_class class method. Like this:
|
22
|
+
#
|
23
|
+
# class AuditObserver < ActiveRecord::Observer
|
24
|
+
# def self.observed_class() Account end
|
25
|
+
# def after_update(account)
|
26
|
+
# AuditTrail.new(account, "UPDATED")
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# == Observing multiple classes at once
|
31
|
+
#
|
32
|
+
# If the audit observer needs to watch more than one kind of object, this can be specified in an array, like this:
|
33
|
+
#
|
34
|
+
# class AuditObserver < ActiveRecord::Observer
|
35
|
+
# def self.observed_class() [ Account, Balance ] end
|
36
|
+
# def after_update(record)
|
37
|
+
# AuditTrail.new(record, "UPDATED")
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# The AuditObserver will now act on both updates to Account and Balance by treating them both as records.
|
42
|
+
#
|
43
|
+
# The observer can implement callback methods for each of the methods described in the Callbacks module.
|
44
|
+
class Observer
|
45
|
+
include Singleton
|
46
|
+
|
47
|
+
def initialize
|
48
|
+
[ observed_class ].flatten.each do |klass|
|
49
|
+
klass.add_observer(self)
|
50
|
+
klass.send(:define_method, :after_find) unless klass.respond_to?(:after_find)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def update(callback_method, object)
|
55
|
+
send(callback_method, object) if respond_to?(callback_method)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
def observed_class
|
60
|
+
if self.class.respond_to? "observed_class"
|
61
|
+
self.class.observed_class
|
62
|
+
else
|
63
|
+
Object.const_get(infer_observed_class_name)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def infer_observed_class_name
|
68
|
+
self.class.name.scan(/(.*)Observer/)[0][0]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Reflection # :nodoc:
|
3
|
+
def self.append_features(base)
|
4
|
+
super
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
|
7
|
+
base.class_eval do
|
8
|
+
class << self
|
9
|
+
alias_method :composed_of_without_reflection, :composed_of
|
10
|
+
|
11
|
+
def composed_of_with_reflection(part_id, options = {})
|
12
|
+
composed_of_without_reflection(part_id, options)
|
13
|
+
write_inheritable_array "aggregations", [ AggregateReflection.new(part_id, options, self) ]
|
14
|
+
end
|
15
|
+
|
16
|
+
alias_method :composed_of, :composed_of_with_reflection
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
for association_type in %w( belongs_to has_one has_many has_and_belongs_to_many )
|
21
|
+
base.module_eval <<-"end_eval"
|
22
|
+
class << self
|
23
|
+
alias_method :#{association_type}_without_reflection, :#{association_type}
|
24
|
+
|
25
|
+
def #{association_type}_with_reflection(association_id, options = {})
|
26
|
+
#{association_type}_without_reflection(association_id, options)
|
27
|
+
write_inheritable_array "associations", [ AssociationReflection.new(association_id, options, self) ]
|
28
|
+
end
|
29
|
+
|
30
|
+
alias_method :#{association_type}, :#{association_type}_with_reflection
|
31
|
+
end
|
32
|
+
end_eval
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Reflection allows you to interrogate Active Record classes and objects about their associations and aggregations.
|
37
|
+
# This information can for example be used in a form builder that took an Active Record object and created input
|
38
|
+
# fields for all of the attributes depending on their type and displayed the associations to other objects.
|
39
|
+
#
|
40
|
+
# You can find the interface for the AggregateReflection and AssociationReflection classes in the abstract MacroReflection class.
|
41
|
+
module ClassMethods
|
42
|
+
# Returns an array of AggregateReflection objects for all the aggregations in the class.
|
43
|
+
def reflect_on_all_aggregations
|
44
|
+
read_inheritable_attribute "aggregations"
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns the AggregateReflection object for the named +aggregation+ (use the symbol). Example:
|
48
|
+
# Account.reflect_on_aggregation(:balance) # returns the balance AggregateReflection
|
49
|
+
def reflect_on_aggregation(aggregation)
|
50
|
+
reflect_on_all_aggregations.find { |reflection| reflection.name == aggregation } unless reflect_on_all_aggregations.nil?
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns an array of AssociationReflection objects for all the aggregations in the class.
|
54
|
+
def reflect_on_all_associations
|
55
|
+
read_inheritable_attribute "associations"
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns the AssociationReflection object for the named +aggregation+ (use the symbol). Example:
|
59
|
+
# Account.reflect_on_association(:owner) # returns the owner AssociationReflection
|
60
|
+
def reflect_on_association(association)
|
61
|
+
reflect_on_all_associations.find { |reflection| reflection.name == association } unless reflect_on_all_associations.nil?
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
# Abstract base class for AggregateReflection and AssociationReflection that describes the interface available for both of
|
67
|
+
# those classes. Objects of AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
|
68
|
+
class MacroReflection
|
69
|
+
attr_reader :active_record
|
70
|
+
def initialize(name, options, active_record)
|
71
|
+
@name, @options, @active_record = name, options, active_record
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns the name of the macro, so it would return :balance for "composed_of :balance, :class_name => 'Money'" or
|
75
|
+
# :clients for "has_many :clients".
|
76
|
+
def name
|
77
|
+
@name
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns the hash of options used for the macro, so it would return { :class_name => "Money" } for
|
81
|
+
# "composed_of :balance, :class_name => 'Money'" or {} for "has_many :clients".
|
82
|
+
def options
|
83
|
+
@options
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns the class for the macro, so "composed_of :balance, :class_name => 'Money'" would return the Money class and
|
87
|
+
# "has_many :clients" would return the Client class.
|
88
|
+
def klass() end
|
89
|
+
|
90
|
+
def ==(other_aggregation)
|
91
|
+
name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
# Holds all the meta-data about an aggregation as it was specified in the Active Record class.
|
97
|
+
class AggregateReflection < MacroReflection #:nodoc:
|
98
|
+
def klass
|
99
|
+
Object.const_get(options[:class_name] || name_to_class_name(name.id2name))
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
def name_to_class_name(name)
|
104
|
+
name.capitalize.gsub(/_(.)/) { |s| $1.capitalize }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Holds all the meta-data about an association as it was specified in the Active Record class.
|
109
|
+
class AssociationReflection < MacroReflection #:nodoc:
|
110
|
+
def klass
|
111
|
+
active_record.send(:compute_type, (name_to_class_name(name.id2name)))
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
def name_to_class_name(name)
|
116
|
+
if name !~ /::/
|
117
|
+
class_name = active_record.send(
|
118
|
+
:type_name_with_module,
|
119
|
+
(options[:class_name] || active_record.class_name(active_record.table_name_prefix + name + active_record.table_name_suffix))
|
120
|
+
)
|
121
|
+
end
|
122
|
+
return class_name || name
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# attr_* style accessors for class-variables that can accessed both on an instance and class level.
|
2
|
+
class Class #:nodoc:
|
3
|
+
def cattr_reader(*syms)
|
4
|
+
syms.each do |sym|
|
5
|
+
class_eval <<-EOS
|
6
|
+
if ! defined? @@#{sym.id2name}
|
7
|
+
@@#{sym.id2name} = nil
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.#{sym.id2name}
|
11
|
+
@@#{sym}
|
12
|
+
end
|
13
|
+
|
14
|
+
def #{sym.id2name}
|
15
|
+
self.class.#{sym.id2name}
|
16
|
+
end
|
17
|
+
EOS
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def cattr_writer(*syms)
|
22
|
+
syms.each do |sym|
|
23
|
+
class_eval <<-EOS
|
24
|
+
if ! defined? @@#{sym.id2name}
|
25
|
+
@@#{sym.id2name} = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.#{sym.id2name}=(obj)
|
29
|
+
@@#{sym.id2name} = obj
|
30
|
+
end
|
31
|
+
|
32
|
+
def #{sym.id2name}=(obj)
|
33
|
+
self.class.#{sym.id2name}=(obj)
|
34
|
+
end
|
35
|
+
EOS
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def cattr_accessor(*syms)
|
40
|
+
cattr_reader(*syms)
|
41
|
+
cattr_writer(*syms)
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# Allows attributes to be shared within an inheritance hierarchy, but where each descentent gets a copy of
|
2
|
+
# their parents' attributes, instead of just a pointer to the same. This means that the child can add elements
|
3
|
+
# to, for example, an array without those additions being shared with either their parent, siblings, or
|
4
|
+
# children, which is unlike the regular class-level attributes that are shared across the entire hierarchy.
|
5
|
+
module ClassInheritableAttributes # :nodoc:
|
6
|
+
def self.append_features(base)
|
7
|
+
super
|
8
|
+
base.extend(ClassMethods)
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods # :nodoc:
|
12
|
+
@@classes ||= {}
|
13
|
+
|
14
|
+
def inheritable_attributes
|
15
|
+
@@classes[self] ||= {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def write_inheritable_attribute(key, value)
|
19
|
+
inheritable_attributes[key] = value
|
20
|
+
end
|
21
|
+
|
22
|
+
def write_inheritable_array(key, elements)
|
23
|
+
write_inheritable_attribute(key, []) if read_inheritable_attribute(key).nil?
|
24
|
+
write_inheritable_attribute(key, read_inheritable_attribute(key) + elements)
|
25
|
+
end
|
26
|
+
|
27
|
+
def read_inheritable_attribute(key)
|
28
|
+
inheritable_attributes[key]
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def inherited(child)
|
33
|
+
@@classes[child] = inheritable_attributes.dup
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|