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.
Files changed (115) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/Gemfile +13 -0
  4. data/LICENSE +22 -0
  5. data/README.md +29 -0
  6. data/bench/all.rb +4 -0
  7. data/bench/factory.rb +68 -0
  8. data/bench/fake_record.rb +174 -0
  9. data/bench/model.rb +201 -0
  10. data/bench/read_model.rb +191 -0
  11. data/bench/results/factory.txt +21 -0
  12. data/bench/results/fake_record.txt +37 -0
  13. data/bench/results/model.txt +44 -0
  14. data/bench/results/read_model.txt +46 -0
  15. data/bench/setup.rb +132 -0
  16. data/lib/mr.rb +11 -0
  17. data/lib/mr/after_commit.rb +49 -0
  18. data/lib/mr/after_commit/fake_record.rb +39 -0
  19. data/lib/mr/after_commit/record.rb +48 -0
  20. data/lib/mr/after_commit/record_procs_methods.rb +82 -0
  21. data/lib/mr/factory.rb +82 -0
  22. data/lib/mr/factory/config.rb +240 -0
  23. data/lib/mr/factory/model_factory.rb +103 -0
  24. data/lib/mr/factory/model_stack.rb +28 -0
  25. data/lib/mr/factory/read_model_factory.rb +104 -0
  26. data/lib/mr/factory/record_factory.rb +130 -0
  27. data/lib/mr/factory/record_stack.rb +219 -0
  28. data/lib/mr/fake_query.rb +53 -0
  29. data/lib/mr/fake_record.rb +58 -0
  30. data/lib/mr/fake_record/associations.rb +257 -0
  31. data/lib/mr/fake_record/attributes.rb +168 -0
  32. data/lib/mr/fake_record/persistence.rb +116 -0
  33. data/lib/mr/json_field.rb +180 -0
  34. data/lib/mr/json_field/fake_record.rb +31 -0
  35. data/lib/mr/json_field/record.rb +38 -0
  36. data/lib/mr/model.rb +67 -0
  37. data/lib/mr/model/associations.rb +161 -0
  38. data/lib/mr/model/configuration.rb +67 -0
  39. data/lib/mr/model/fields.rb +177 -0
  40. data/lib/mr/model/persistence.rb +79 -0
  41. data/lib/mr/query.rb +126 -0
  42. data/lib/mr/read_model.rb +83 -0
  43. data/lib/mr/read_model/data.rb +38 -0
  44. data/lib/mr/read_model/fields.rb +218 -0
  45. data/lib/mr/read_model/query_expression.rb +188 -0
  46. data/lib/mr/read_model/querying.rb +214 -0
  47. data/lib/mr/read_model/set_querying.rb +82 -0
  48. data/lib/mr/read_model/subquery.rb +98 -0
  49. data/lib/mr/record.rb +35 -0
  50. data/lib/mr/test_helpers.rb +229 -0
  51. data/lib/mr/type_converter.rb +85 -0
  52. data/lib/mr/version.rb +3 -0
  53. data/log/.gitkeep +0 -0
  54. data/mr.gemspec +29 -0
  55. data/test/helper.rb +21 -0
  56. data/test/support/db.rb +10 -0
  57. data/test/support/factory.rb +13 -0
  58. data/test/support/factory/area.rb +6 -0
  59. data/test/support/factory/comment.rb +14 -0
  60. data/test/support/factory/image.rb +6 -0
  61. data/test/support/factory/user.rb +6 -0
  62. data/test/support/models/area.rb +58 -0
  63. data/test/support/models/comment.rb +60 -0
  64. data/test/support/models/image.rb +53 -0
  65. data/test/support/models/user.rb +96 -0
  66. data/test/support/read_model/querying.rb +150 -0
  67. data/test/support/read_models/comment_with_user_data.rb +27 -0
  68. data/test/support/read_models/set_data.rb +49 -0
  69. data/test/support/read_models/subquery_data.rb +41 -0
  70. data/test/support/read_models/user_with_area_data.rb +15 -0
  71. data/test/support/schema.rb +39 -0
  72. data/test/support/setup_test_db.rb +10 -0
  73. data/test/system/factory/model_factory_tests.rb +87 -0
  74. data/test/system/factory/model_stack_tests.rb +30 -0
  75. data/test/system/factory/record_factory_tests.rb +84 -0
  76. data/test/system/factory/record_stack_tests.rb +51 -0
  77. data/test/system/factory_tests.rb +32 -0
  78. data/test/system/read_model_tests.rb +199 -0
  79. data/test/system/with_model_tests.rb +275 -0
  80. data/test/unit/after_commit/fake_record_tests.rb +110 -0
  81. data/test/unit/after_commit/record_procs_methods_tests.rb +177 -0
  82. data/test/unit/after_commit/record_tests.rb +134 -0
  83. data/test/unit/after_commit_tests.rb +113 -0
  84. data/test/unit/factory/config_tests.rb +651 -0
  85. data/test/unit/factory/model_factory_tests.rb +473 -0
  86. data/test/unit/factory/model_stack_tests.rb +97 -0
  87. data/test/unit/factory/read_model_factory_tests.rb +195 -0
  88. data/test/unit/factory/record_factory_tests.rb +446 -0
  89. data/test/unit/factory/record_stack_tests.rb +549 -0
  90. data/test/unit/factory_tests.rb +213 -0
  91. data/test/unit/fake_query_tests.rb +137 -0
  92. data/test/unit/fake_record/associations_tests.rb +585 -0
  93. data/test/unit/fake_record/attributes_tests.rb +265 -0
  94. data/test/unit/fake_record/persistence_tests.rb +239 -0
  95. data/test/unit/fake_record_tests.rb +106 -0
  96. data/test/unit/json_field/fake_record_tests.rb +75 -0
  97. data/test/unit/json_field/record_tests.rb +80 -0
  98. data/test/unit/json_field_tests.rb +302 -0
  99. data/test/unit/model/associations_tests.rb +346 -0
  100. data/test/unit/model/configuration_tests.rb +92 -0
  101. data/test/unit/model/fields_tests.rb +278 -0
  102. data/test/unit/model/persistence_tests.rb +114 -0
  103. data/test/unit/model_tests.rb +137 -0
  104. data/test/unit/query_tests.rb +300 -0
  105. data/test/unit/read_model/data_tests.rb +56 -0
  106. data/test/unit/read_model/fields_tests.rb +416 -0
  107. data/test/unit/read_model/query_expression_tests.rb +381 -0
  108. data/test/unit/read_model/querying_tests.rb +613 -0
  109. data/test/unit/read_model/set_querying_tests.rb +149 -0
  110. data/test/unit/read_model/subquery_tests.rb +242 -0
  111. data/test/unit/read_model_tests.rb +187 -0
  112. data/test/unit/record_tests.rb +45 -0
  113. data/test/unit/test_helpers_tests.rb +431 -0
  114. data/test/unit/type_converter_tests.rb +207 -0
  115. 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
@@ -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