mr 0.35.2
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.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/Gemfile +13 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/bench/all.rb +4 -0
- data/bench/factory.rb +68 -0
- data/bench/fake_record.rb +174 -0
- data/bench/model.rb +201 -0
- data/bench/read_model.rb +191 -0
- data/bench/results/factory.txt +21 -0
- data/bench/results/fake_record.txt +37 -0
- data/bench/results/model.txt +44 -0
- data/bench/results/read_model.txt +46 -0
- data/bench/setup.rb +132 -0
- data/lib/mr.rb +11 -0
- data/lib/mr/after_commit.rb +49 -0
- data/lib/mr/after_commit/fake_record.rb +39 -0
- data/lib/mr/after_commit/record.rb +48 -0
- data/lib/mr/after_commit/record_procs_methods.rb +82 -0
- data/lib/mr/factory.rb +82 -0
- data/lib/mr/factory/config.rb +240 -0
- data/lib/mr/factory/model_factory.rb +103 -0
- data/lib/mr/factory/model_stack.rb +28 -0
- data/lib/mr/factory/read_model_factory.rb +104 -0
- data/lib/mr/factory/record_factory.rb +130 -0
- data/lib/mr/factory/record_stack.rb +219 -0
- data/lib/mr/fake_query.rb +53 -0
- data/lib/mr/fake_record.rb +58 -0
- data/lib/mr/fake_record/associations.rb +257 -0
- data/lib/mr/fake_record/attributes.rb +168 -0
- data/lib/mr/fake_record/persistence.rb +116 -0
- data/lib/mr/json_field.rb +180 -0
- data/lib/mr/json_field/fake_record.rb +31 -0
- data/lib/mr/json_field/record.rb +38 -0
- data/lib/mr/model.rb +67 -0
- data/lib/mr/model/associations.rb +161 -0
- data/lib/mr/model/configuration.rb +67 -0
- data/lib/mr/model/fields.rb +177 -0
- data/lib/mr/model/persistence.rb +79 -0
- data/lib/mr/query.rb +126 -0
- data/lib/mr/read_model.rb +83 -0
- data/lib/mr/read_model/data.rb +38 -0
- data/lib/mr/read_model/fields.rb +218 -0
- data/lib/mr/read_model/query_expression.rb +188 -0
- data/lib/mr/read_model/querying.rb +214 -0
- data/lib/mr/read_model/set_querying.rb +82 -0
- data/lib/mr/read_model/subquery.rb +98 -0
- data/lib/mr/record.rb +35 -0
- data/lib/mr/test_helpers.rb +229 -0
- data/lib/mr/type_converter.rb +85 -0
- data/lib/mr/version.rb +3 -0
- data/log/.gitkeep +0 -0
- data/mr.gemspec +29 -0
- data/test/helper.rb +21 -0
- data/test/support/db.rb +10 -0
- data/test/support/factory.rb +13 -0
- data/test/support/factory/area.rb +6 -0
- data/test/support/factory/comment.rb +14 -0
- data/test/support/factory/image.rb +6 -0
- data/test/support/factory/user.rb +6 -0
- data/test/support/models/area.rb +58 -0
- data/test/support/models/comment.rb +60 -0
- data/test/support/models/image.rb +53 -0
- data/test/support/models/user.rb +96 -0
- data/test/support/read_model/querying.rb +150 -0
- data/test/support/read_models/comment_with_user_data.rb +27 -0
- data/test/support/read_models/set_data.rb +49 -0
- data/test/support/read_models/subquery_data.rb +41 -0
- data/test/support/read_models/user_with_area_data.rb +15 -0
- data/test/support/schema.rb +39 -0
- data/test/support/setup_test_db.rb +10 -0
- data/test/system/factory/model_factory_tests.rb +87 -0
- data/test/system/factory/model_stack_tests.rb +30 -0
- data/test/system/factory/record_factory_tests.rb +84 -0
- data/test/system/factory/record_stack_tests.rb +51 -0
- data/test/system/factory_tests.rb +32 -0
- data/test/system/read_model_tests.rb +199 -0
- data/test/system/with_model_tests.rb +275 -0
- data/test/unit/after_commit/fake_record_tests.rb +110 -0
- data/test/unit/after_commit/record_procs_methods_tests.rb +177 -0
- data/test/unit/after_commit/record_tests.rb +134 -0
- data/test/unit/after_commit_tests.rb +113 -0
- data/test/unit/factory/config_tests.rb +651 -0
- data/test/unit/factory/model_factory_tests.rb +473 -0
- data/test/unit/factory/model_stack_tests.rb +97 -0
- data/test/unit/factory/read_model_factory_tests.rb +195 -0
- data/test/unit/factory/record_factory_tests.rb +446 -0
- data/test/unit/factory/record_stack_tests.rb +549 -0
- data/test/unit/factory_tests.rb +213 -0
- data/test/unit/fake_query_tests.rb +137 -0
- data/test/unit/fake_record/associations_tests.rb +585 -0
- data/test/unit/fake_record/attributes_tests.rb +265 -0
- data/test/unit/fake_record/persistence_tests.rb +239 -0
- data/test/unit/fake_record_tests.rb +106 -0
- data/test/unit/json_field/fake_record_tests.rb +75 -0
- data/test/unit/json_field/record_tests.rb +80 -0
- data/test/unit/json_field_tests.rb +302 -0
- data/test/unit/model/associations_tests.rb +346 -0
- data/test/unit/model/configuration_tests.rb +92 -0
- data/test/unit/model/fields_tests.rb +278 -0
- data/test/unit/model/persistence_tests.rb +114 -0
- data/test/unit/model_tests.rb +137 -0
- data/test/unit/query_tests.rb +300 -0
- data/test/unit/read_model/data_tests.rb +56 -0
- data/test/unit/read_model/fields_tests.rb +416 -0
- data/test/unit/read_model/query_expression_tests.rb +381 -0
- data/test/unit/read_model/querying_tests.rb +613 -0
- data/test/unit/read_model/set_querying_tests.rb +149 -0
- data/test/unit/read_model/subquery_tests.rb +242 -0
- data/test/unit/read_model_tests.rb +187 -0
- data/test/unit/record_tests.rb +45 -0
- data/test/unit/test_helpers_tests.rb +431 -0
- data/test/unit/type_converter_tests.rb +207 -0
- metadata +285 -0
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
require 'active_record'
|
3
|
+
require 'active_record/validations'
|
4
|
+
require 'much-plugin'
|
5
|
+
require 'mr/factory'
|
6
|
+
require 'mr/fake_record/attributes'
|
7
|
+
|
8
|
+
module MR; end
|
9
|
+
module MR::FakeRecord
|
10
|
+
|
11
|
+
module Persistence
|
12
|
+
include MuchPlugin
|
13
|
+
|
14
|
+
plugin_included do
|
15
|
+
include MR::FakeRecord::Attributes
|
16
|
+
extend TransactionMethods
|
17
|
+
extend ClassMethods
|
18
|
+
include InstanceMethods
|
19
|
+
|
20
|
+
attribute :id, :primary_key
|
21
|
+
end
|
22
|
+
|
23
|
+
# this is broken into a separate module so `FakeRecord` can extend it and
|
24
|
+
# provide easy access to the `transaction` method (for stubbing, etc)
|
25
|
+
module TransactionMethods
|
26
|
+
|
27
|
+
# ActiveRecord methods
|
28
|
+
|
29
|
+
def transaction
|
30
|
+
begin
|
31
|
+
yield if block_given?
|
32
|
+
rescue ActiveRecord::Rollback
|
33
|
+
# activerecord swallows rollback exceptions, they are only intended as a
|
34
|
+
# mechanism to rollback the transaction
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
module ClassMethods
|
41
|
+
|
42
|
+
# ActiveRecord methods
|
43
|
+
|
44
|
+
# this is needed to raise ActiveRecord::RecordInvalid
|
45
|
+
def human_attribute_name(attribute, options = {})
|
46
|
+
options[:default] || attribute.to_s.split('.').last
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
module InstanceMethods
|
52
|
+
|
53
|
+
# ActiveRecord methods
|
54
|
+
|
55
|
+
def save!
|
56
|
+
raise ActiveRecord::RecordInvalid.new(self) unless self.valid?
|
57
|
+
self.id ||= MR::Factory.primary_key(self.class)
|
58
|
+
current_time = CurrentTime.new
|
59
|
+
self.created_at ||= current_time if self.respond_to?(:created_at=)
|
60
|
+
if self.respond_to?(:updated_at=) && !self.updated_at_changed?
|
61
|
+
self.updated_at = current_time
|
62
|
+
end
|
63
|
+
self.saved_attributes = self.attributes.dup
|
64
|
+
@save_called = true
|
65
|
+
end
|
66
|
+
|
67
|
+
def destroy
|
68
|
+
@destroyed = true
|
69
|
+
end
|
70
|
+
|
71
|
+
def transaction(&block)
|
72
|
+
self.class.transaction(&block)
|
73
|
+
end
|
74
|
+
|
75
|
+
def new_record?
|
76
|
+
!self.id
|
77
|
+
end
|
78
|
+
|
79
|
+
def destroyed?
|
80
|
+
!!@destroyed
|
81
|
+
end
|
82
|
+
|
83
|
+
def errors
|
84
|
+
@errors ||= ActiveModel::Errors.new(self)
|
85
|
+
end
|
86
|
+
|
87
|
+
def valid?
|
88
|
+
self.errors.empty?
|
89
|
+
end
|
90
|
+
|
91
|
+
# Non-ActiveRecord methods
|
92
|
+
|
93
|
+
def save_called
|
94
|
+
@save_called = false if @save_called.nil?
|
95
|
+
@save_called
|
96
|
+
end
|
97
|
+
|
98
|
+
def reset_save_called
|
99
|
+
@save_called = false
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
module CurrentTime
|
105
|
+
def self.new
|
106
|
+
if ActiveRecord::Base.default_timezone == :utc
|
107
|
+
Time.now.utc
|
108
|
+
else
|
109
|
+
Time.now
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
require 'much-plugin'
|
2
|
+
|
3
|
+
require 'mr/json_field/fake_record'
|
4
|
+
require 'mr/json_field/record'
|
5
|
+
|
6
|
+
module MR
|
7
|
+
|
8
|
+
module JsonField
|
9
|
+
include MuchPlugin
|
10
|
+
|
11
|
+
DEFAULT_ENCODER = proc{ |value| ::JSON.dump(value) }
|
12
|
+
DEFAULT_DECODER = proc{ |value| ::JSON.load(value) }
|
13
|
+
|
14
|
+
def self.encoder; @encoder ||= DEFAULT_ENCODER; end
|
15
|
+
def self.encoder=(new_value); @encoder = new_value; end
|
16
|
+
|
17
|
+
def self.decoder; @decoder ||= DEFAULT_DECODER; end
|
18
|
+
def self.decoder=(new_value); @decoder = new_value; end
|
19
|
+
|
20
|
+
def self.encode(value)
|
21
|
+
self.encoder.call(value)
|
22
|
+
rescue StandardError => exception
|
23
|
+
raise InvalidJSONError, exception.message
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.decode(value)
|
27
|
+
self.decoder.call(value)
|
28
|
+
rescue StandardError => exception
|
29
|
+
raise InvalidJSONError, exception.message
|
30
|
+
end
|
31
|
+
|
32
|
+
# this can be used with `MR::Model` or `MR::ReadModel`, so it doesn't
|
33
|
+
# include either by default
|
34
|
+
plugin_included do
|
35
|
+
extend ClassMethods
|
36
|
+
end
|
37
|
+
|
38
|
+
module ClassMethods
|
39
|
+
|
40
|
+
def json_field(field_name, options = nil)
|
41
|
+
json_field_reader(field_name, options)
|
42
|
+
json_field_writer(field_name, options)
|
43
|
+
end
|
44
|
+
|
45
|
+
def json_field_reader(field_name, options = nil)
|
46
|
+
options ||= {}
|
47
|
+
field_name = field_name.to_s
|
48
|
+
ivar_name = "@#{field_name}"
|
49
|
+
source_field_name = (options[:source] || "#{field_name}_json").to_s
|
50
|
+
|
51
|
+
if source_field_name == field_name
|
52
|
+
raise ArgumentError, "the field name and source cannot be the same"
|
53
|
+
end
|
54
|
+
|
55
|
+
define_method(field_name) do
|
56
|
+
if !(cached_value = self.instance_variable_get(ivar_name)).nil?
|
57
|
+
return cached_value
|
58
|
+
else
|
59
|
+
source_value = self.send(source_field_name)
|
60
|
+
return source_value if source_value.nil?
|
61
|
+
|
62
|
+
value = begin
|
63
|
+
MR::JsonField.decode(source_value)
|
64
|
+
rescue InvalidJSONError => exception
|
65
|
+
message = "can't decode `#{field_name}` JSON field (from " \
|
66
|
+
"`#{source_field_name}`): #{exception.message}"
|
67
|
+
raise exception.class, message, caller
|
68
|
+
end
|
69
|
+
|
70
|
+
# cache the decoded value in the ivar
|
71
|
+
self.instance_variable_set(ivar_name, value)
|
72
|
+
|
73
|
+
value
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
(self.json_field_readers << field_name).uniq!
|
78
|
+
(self.json_field_source_fields << source_field_name).uniq!
|
79
|
+
end
|
80
|
+
|
81
|
+
def json_field_writer(field_name, options = nil)
|
82
|
+
options ||= {}
|
83
|
+
field_name = field_name.to_s.strip
|
84
|
+
ivar_name = "@#{field_name}"
|
85
|
+
source_field_name = (options[:source] || "#{field_name}_json").to_s.strip
|
86
|
+
|
87
|
+
if source_field_name == field_name
|
88
|
+
raise ArgumentError, "the field name and source cannot be the same"
|
89
|
+
end
|
90
|
+
|
91
|
+
define_method("#{field_name}=") do |new_value|
|
92
|
+
encoded_value = if !new_value.nil?
|
93
|
+
begin
|
94
|
+
MR::JsonField.encode(new_value)
|
95
|
+
rescue InvalidJSONError => exception
|
96
|
+
message = "can't encode value for `#{field_name}` JSON field: " \
|
97
|
+
"#{exception.message}"
|
98
|
+
raise exception.class, message, caller
|
99
|
+
end
|
100
|
+
else
|
101
|
+
nil
|
102
|
+
end
|
103
|
+
self.send("#{source_field_name}=", encoded_value)
|
104
|
+
|
105
|
+
# reset the ivar so its value will be calculated again when read
|
106
|
+
self.instance_variable_set(ivar_name, nil)
|
107
|
+
|
108
|
+
new_value
|
109
|
+
end
|
110
|
+
|
111
|
+
(self.json_field_writers << field_name).uniq!
|
112
|
+
(self.json_field_source_fields << source_field_name).uniq!
|
113
|
+
end
|
114
|
+
|
115
|
+
def json_field_readers; @json_field_readers ||= []; end
|
116
|
+
def json_field_writers; @json_field_writers ||= []; end
|
117
|
+
|
118
|
+
def json_field_accessors
|
119
|
+
self.json_field_readers & self.json_field_writers
|
120
|
+
end
|
121
|
+
|
122
|
+
def json_field_source_fields
|
123
|
+
@json_field_source_fields ||= []
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
module TestHelpers
|
129
|
+
include MuchPlugin
|
130
|
+
|
131
|
+
plugin_included do
|
132
|
+
include InstanceMethods
|
133
|
+
|
134
|
+
require 'mr/factory'
|
135
|
+
end
|
136
|
+
|
137
|
+
module InstanceMethods
|
138
|
+
|
139
|
+
def assert_json_field(subject, field_name, options = nil)
|
140
|
+
assert_json_field_reader(subject, field_name, options)
|
141
|
+
assert_json_field_writer(subject, field_name, options)
|
142
|
+
end
|
143
|
+
|
144
|
+
def assert_json_field_reader(subject, field_name, options = nil)
|
145
|
+
options ||= {}
|
146
|
+
source_field_name = options[:source] || "#{field_name}_json"
|
147
|
+
|
148
|
+
# set a value to read if it's `nil`
|
149
|
+
if subject.send(source_field_name).nil?
|
150
|
+
encoded_value = MR::JsonField.encode({
|
151
|
+
MR::Factory.string => MR::Factory.string
|
152
|
+
})
|
153
|
+
subject.send("#{source_field_name}=", encoded_value)
|
154
|
+
end
|
155
|
+
|
156
|
+
assert_respond_to "#{field_name}", subject
|
157
|
+
exp = MR::JsonField.decode(subject.send"#{source_field_name}")
|
158
|
+
assert_equal exp, subject.send("#{field_name}")
|
159
|
+
end
|
160
|
+
|
161
|
+
def assert_json_field_writer(subject, field_name, options = nil)
|
162
|
+
options ||= {}
|
163
|
+
source_field_name = options[:source] || "#{field_name}_json"
|
164
|
+
|
165
|
+
assert_respond_to "#{field_name}=", subject
|
166
|
+
new_value = { MR::Factory.string => MR::Factory.string }
|
167
|
+
subject.send("#{field_name}=", new_value)
|
168
|
+
exp = MR::JsonField.encode(new_value)
|
169
|
+
assert_equal exp, subject.send("#{source_field_name}")
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
InvalidJSONError = Class.new(StandardError)
|
177
|
+
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'much-plugin'
|
2
|
+
|
3
|
+
require 'mr/fake_record'
|
4
|
+
|
5
|
+
module MR; end
|
6
|
+
module MR::JsonField
|
7
|
+
|
8
|
+
module FakeRecord
|
9
|
+
include MuchPlugin
|
10
|
+
|
11
|
+
plugin_included do
|
12
|
+
include MR::FakeRecord
|
13
|
+
include InstanceMethods
|
14
|
+
end
|
15
|
+
|
16
|
+
module InstanceMethods
|
17
|
+
|
18
|
+
# this mimics the `JsonField::Record` mixin, doing the same logic to
|
19
|
+
# ensure that the source fields match the json fields
|
20
|
+
def save!
|
21
|
+
self.model.class.json_field_accessors.each do |field_name|
|
22
|
+
self.model.send("#{field_name}=", self.model.send(field_name))
|
23
|
+
end
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'much-plugin'
|
2
|
+
|
3
|
+
require 'mr/record'
|
4
|
+
|
5
|
+
module MR; end
|
6
|
+
module MR::JsonField
|
7
|
+
|
8
|
+
module Record
|
9
|
+
include MuchPlugin
|
10
|
+
|
11
|
+
plugin_included do
|
12
|
+
include MR::Record
|
13
|
+
include InstanceMethods
|
14
|
+
|
15
|
+
before_save :json_field_sync_all_json_fields_on_save
|
16
|
+
end
|
17
|
+
|
18
|
+
module InstanceMethods
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
# this makes it so changes that are done to the JSON object are persisted
|
23
|
+
# to the DB without having to re-write the entire field, for example if
|
24
|
+
# the json field is a hash then a change to an individual key's value
|
25
|
+
# needs to get re-encoded and stored in the source field, since the json
|
26
|
+
# field writer wasn't used it won't get encoded, this callback fixes the
|
27
|
+
# issue by ensuring all json fields get synced before we save the record
|
28
|
+
def json_field_sync_all_json_fields_on_save
|
29
|
+
self.model.class.json_field_accessors.each do |field_name|
|
30
|
+
self.model.send("#{field_name}=", self.model.send(field_name))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
data/lib/mr/model.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'much-plugin'
|
2
|
+
require 'mr/model/associations'
|
3
|
+
require 'mr/model/configuration'
|
4
|
+
require 'mr/model/fields'
|
5
|
+
require 'mr/model/persistence'
|
6
|
+
|
7
|
+
module MR
|
8
|
+
|
9
|
+
module Model
|
10
|
+
include MuchPlugin
|
11
|
+
|
12
|
+
plugin_included do
|
13
|
+
include MR::Model::Configuration
|
14
|
+
include MR::Model::Fields
|
15
|
+
include MR::Model::Associations
|
16
|
+
include MR::Model::Persistence
|
17
|
+
extend ClassMethods
|
18
|
+
include InstanceMethods
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
|
23
|
+
def find(id)
|
24
|
+
self.new(self.record_class.find(id))
|
25
|
+
end
|
26
|
+
|
27
|
+
def all
|
28
|
+
self.record_class.all.map{ |record| self.new(record) }
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
module InstanceMethods
|
34
|
+
|
35
|
+
def initialize(*args)
|
36
|
+
field_values = args.pop if args.last.kind_of?(Hash)
|
37
|
+
set_record(args.first || self.record_class.new)
|
38
|
+
(field_values || {}).each do |name, value|
|
39
|
+
self.send("#{name}=", value)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def ==(other)
|
44
|
+
other.kind_of?(self.class) ? self.record == other.record : super
|
45
|
+
end
|
46
|
+
|
47
|
+
def eql?(other)
|
48
|
+
other.kind_of?(self.class) ? self.record.eql?(other.record) : super
|
49
|
+
end
|
50
|
+
|
51
|
+
def hash
|
52
|
+
record.hash
|
53
|
+
end
|
54
|
+
|
55
|
+
def inspect
|
56
|
+
object_hex = (self.object_id << 1).to_s(16)
|
57
|
+
fields_inspect = self.class.fields.map do |field|
|
58
|
+
"@#{field.name}=#{field.read(record).inspect}"
|
59
|
+
end.sort.join(" ")
|
60
|
+
"#<#{self.class}:0x#{object_hex} #{fields_inspect}>"
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'much-plugin'
|
2
|
+
require 'mr/model/configuration'
|
3
|
+
|
4
|
+
module MR; end
|
5
|
+
module MR::Model
|
6
|
+
|
7
|
+
module Associations
|
8
|
+
include MuchPlugin
|
9
|
+
|
10
|
+
plugin_included do
|
11
|
+
include MR::Model::Configuration
|
12
|
+
extend ClassMethods
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
|
17
|
+
def associations
|
18
|
+
@associations ||= MR::Model::AssociationSet.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def belongs_to(*names)
|
22
|
+
names.each do |name|
|
23
|
+
self.associations.add_belongs_to(name, self)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def polymorphic_belongs_to(*names)
|
28
|
+
names.each do |name|
|
29
|
+
self.associations.add_polymorphic_belongs_to(name, self)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def has_one(*names)
|
34
|
+
names.each do |name|
|
35
|
+
self.associations.add_has_one(name, self)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def has_many(*names)
|
40
|
+
names.each do |name|
|
41
|
+
self.associations.add_has_many(name, self)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
class AssociationSet
|
50
|
+
attr_reader :belongs_to, :polymorphic_belongs_to
|
51
|
+
attr_reader :has_one, :has_many
|
52
|
+
|
53
|
+
def initialize
|
54
|
+
@belongs_to = []
|
55
|
+
@polymorphic_belongs_to = []
|
56
|
+
@has_one = []
|
57
|
+
@has_many = []
|
58
|
+
end
|
59
|
+
|
60
|
+
def add_belongs_to(name, model_class)
|
61
|
+
association = BelongsToAssociation.new(name)
|
62
|
+
association.define_accessor_on(model_class)
|
63
|
+
@belongs_to << association
|
64
|
+
end
|
65
|
+
|
66
|
+
def add_polymorphic_belongs_to(name, model_class)
|
67
|
+
association = PolymorphicBelongsToAssociation.new(name)
|
68
|
+
association.define_accessor_on(model_class)
|
69
|
+
@polymorphic_belongs_to << association
|
70
|
+
end
|
71
|
+
|
72
|
+
def add_has_one(name, model_class)
|
73
|
+
association = HasOneAssociation.new(name)
|
74
|
+
association.define_accessor_on(model_class)
|
75
|
+
@has_one << association
|
76
|
+
end
|
77
|
+
|
78
|
+
def add_has_many(name, model_class)
|
79
|
+
association = HasManyAssociation.new(name)
|
80
|
+
association.define_accessor_on(model_class)
|
81
|
+
@has_many << association
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
class Association
|
87
|
+
attr_reader :name
|
88
|
+
attr_reader :reader_method_name, :writer_method_name
|
89
|
+
|
90
|
+
def initialize(name)
|
91
|
+
@name = name.to_s
|
92
|
+
@reader_method_name = @name
|
93
|
+
@writer_method_name = "#{@name}="
|
94
|
+
@association_reader_name = @name
|
95
|
+
@association_writer_name = "#{@name}="
|
96
|
+
end
|
97
|
+
|
98
|
+
def define_accessor_on(model_class)
|
99
|
+
association = self
|
100
|
+
model_class.class_eval do
|
101
|
+
|
102
|
+
define_method(association.reader_method_name) do
|
103
|
+
association.read(record)
|
104
|
+
end
|
105
|
+
|
106
|
+
define_method(association.writer_method_name) do |value|
|
107
|
+
begin
|
108
|
+
association.write(value, self, record){ |m| m.record }
|
109
|
+
rescue BadAssociationValueError => exception
|
110
|
+
raise ArgumentError, exception.message, caller
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
class OneToOneAssociation < Association
|
119
|
+
def read(record)
|
120
|
+
if associated_record = record.send(@association_reader_name)
|
121
|
+
associated_record.model_class.new(associated_record)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def write(value, model, record, &block)
|
126
|
+
raise BadAssociationValueError.new(value) if value && !value.kind_of?(MR::Model)
|
127
|
+
associated_record = model.instance_exec(value, &block) if value
|
128
|
+
record.send(@association_writer_name, associated_record)
|
129
|
+
value
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
class OneToManyAssociation < Association
|
134
|
+
def read(record)
|
135
|
+
(record.send(@association_reader_name) || []).map do |associated_record|
|
136
|
+
associated_record.model_class.new(associated_record)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def write(values, model, record, &block)
|
141
|
+
associated_records = [*values].compact.map do |value|
|
142
|
+
raise BadAssociationValueError.new(value) if !value.kind_of?(MR::Model)
|
143
|
+
model.instance_exec(value, &block)
|
144
|
+
end
|
145
|
+
record.send(@association_writer_name, associated_records)
|
146
|
+
values
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
BelongsToAssociation = Class.new(OneToOneAssociation)
|
151
|
+
PolymorphicBelongsToAssociation = Class.new(BelongsToAssociation)
|
152
|
+
HasOneAssociation = Class.new(OneToOneAssociation)
|
153
|
+
HasManyAssociation = Class.new(OneToManyAssociation)
|
154
|
+
|
155
|
+
class BadAssociationValueError < RuntimeError
|
156
|
+
def initialize(value)
|
157
|
+
super "#{value.inspect} is not a kind of MR::Model"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|