vorpal 0.1.0.rc3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 824912a10087771cda48b9ad66dac75861e80bb1
4
- data.tar.gz: 67ff5e87b3e45c8308866036e79cfb83ae5959d8
2
+ SHA256:
3
+ metadata.gz: c866f3e83daac73f28e5c9a53875b73d7f8db95cbf437200f0b0c69cce81d317
4
+ data.tar.gz: e1e91f848552e1f3710849370978d4a1e322e65b12c65bebcd995b76960ba44e
5
5
  SHA512:
6
- metadata.gz: 3db514381c185a0dc4b9b4a408ca35b34a244bb1fe97e899ff1b89ebb84d14cd20d5159c2a459316d77c1e3b4d81e4529e0ff110d02dfdece3f06817531653b1
7
- data.tar.gz: 5437e2611a1ae1e75b1143cd1f9732429fa9acb0fb5e11fc105e60bb7d0330fe4e23021ca7c63b676758977fedeafaed2b39f26be24f83b1f431fc5c7ab1a0b2
6
+ metadata.gz: 5bcd5518f56b4c43f4180d3f63c75c6186c1bd89bee939b8410e656f3df411920238eaf51da4a590367f929507c3bdb1ada98cf972d6ebf6785432417018b9a2
7
+ data.tar.gz: 85e53e6946ea0d0f06e14d8913ac3eba45668b3d08bfc0036a789620ea05af587cb5a97991467eadfea1f76d5dd3fccaf71c96aeec2345327d9538e714e0e720
data/README.md CHANGED
@@ -1,16 +1,13 @@
1
- # Vorpal [![Build Status](https://travis-ci.org/nulogy/vorpal.svg?branch=master)](https://travis-ci.org/nulogy/vorpal) [![Code Climate](https://codeclimate.com/github/nulogy/vorpal/badges/gpa.svg)](https://codeclimate.com/github/nulogy/vorpal)
1
+ # Vorpal [![Build Status](https://travis-ci.com/nulogy/vorpal.svg?branch=master)](https://travis-ci.com/nulogy/vorpal) [![Code Climate](https://codeclimate.com/github/nulogy/vorpal/badges/gpa.svg)](https://codeclimate.com/github/nulogy/vorpal) [![Code Coverage](https://codecov.io/gh/nulogy/vorpal/branch/master/graph/badge.svg)](https://codecov.io/gh/nulogy/vorpal/branch/master)
2
2
 
3
3
  Separate your domain model from your persistence mechanism. Some problems call for a really sharp tool.
4
4
 
5
-
6
- > One, two! One, two! and through and through
7
-
8
- > The vorpal blade went snicker-snack!
9
-
10
- > He left it dead, and with its head
11
-
5
+ > One, two! One, two! and through and through<br/>
6
+ > The vorpal blade went snicker-snack!<br/>
7
+ > He left it dead, and with its head<br/>
12
8
  > He went galumphing back.
13
9
 
10
+ \- [Jabberwocky](https://www.poetryfoundation.org/poems/42916/jabberwocky) by Lewis Carroll
14
11
 
15
12
  ## Overview
16
13
  Vorpal is a [Data Mapper](http://martinfowler.com/eaaCatalog/dataMapper.html)-style ORM (object relational mapper) framelet that persists POROs (plain old Ruby objects) to a relational DB. It has been heavily influenced by concepts from [Domain Driven Design](http://www.infoq.com/minibooks/domain-driven-design-quickly).
@@ -45,8 +42,6 @@ Or install it yourself as:
45
42
 
46
43
  ## Usage
47
44
 
48
- **Warning! API still in flux! Expect it to change with every release until 0.1.0. After this point, semantic versioning will be used.**
49
-
50
45
  Start with a domain model of POROs and AR::Base objects that form an aggregate:
51
46
 
52
47
  ```ruby
@@ -145,7 +140,8 @@ module TreeRepository
145
140
  end
146
141
  ```
147
142
 
148
- Here we've used the `owned` flag on the `belongs_to` from the Tree to the Gardener to show that the Gardener is on the aggregate boundary.
143
+ Here we've used the `owned: false` flag on the `belongs_to` from the Tree to the Gardener to show
144
+ that the Gardener is on the aggregate boundary.
149
145
 
150
146
  And use it:
151
147
 
@@ -176,7 +172,7 @@ It also does not do some things that you might expect from other ORMs:
176
172
  1. No lazy loading of associations. This might sound like a big deal, but with [correctly designed aggregates](http://dddcommunity.org/library/vernon_2011/) it turns out not to be.
177
173
  1. No managing of transactions. It is the strong opinion of the authors that managing transactions is an application-level concern.
178
174
  1. No support for validations. Validations are not a persistence concern.
179
- 1. No AR-style callbacks. Use Infrastructure, Application, or Domain [services](http://martinfowler.com/bliki/EvansClassification.html) instead.
175
+ 1. No AR-style callbacks. Use [Infrastructure, Application, or Domain services](http://martinfowler.com/bliki/EvansClassification.html) instead.
180
176
  1. No has-many-through associations. Use two has-many associations to a join entity instead.
181
177
  1. The `id` attribute is reserved for database primary keys. If you have a natural key/id on your domain model, name it something that makes sense for your domain. It is the strong opinion of the authors that using natural keys as foreign keys is a bad idea. This mixes domain and persistence concerns.
182
178
 
@@ -185,13 +181,11 @@ It also does not do some things that you might expect from other ORMs:
185
181
  1. Only supports PostgreSQL.
186
182
 
187
183
  ## Future Enhancements
188
- * Aggregate updated_at.
189
- * Support for other DBMSs (no MySQL support until ids can be generated without inserting into a table!)
190
- * Support for other ORMs.
191
- * Value objects.
192
- * Remove dependency on ActiveRecord (optimistic locking? updated_at, created_at support? Data type conversions? TimeZone support?)
193
- * More efficient updates (use fewer queries.)
184
+ * Support for UUID primary keys.
194
185
  * Nicer DSL for specifying attributes that have different names in the domain model than in the DB.
186
+ * Show how to implement POROs without using Virtus (it is unsupported and can be crazy slow)
187
+ * Aggregate updated_at.
188
+ * Better support for value objects.
195
189
 
196
190
  ## FAQ
197
191
 
@@ -207,11 +201,12 @@ It also does not do some things that you might expect from other ORMs:
207
201
 
208
202
  **A.** Create a method on a [Repository](http://martinfowler.com/eaaCatalog/repository.html)! They have full access to the DB/ORM so you can use [Arel](https://github.com/rails/arel) and go [crazy](http://asciicasts.com/episodes/239-activerecord-relation-walkthrough) or use direct SQL if you want.
209
203
 
210
- For example:
204
+ For example, use the [#query](https://rubydoc.info/github/nulogy/vorpal/master/Vorpal/AggregateMapper#query-instance_method) method on the [AggregateMapper](https://rubydoc.info/github/nulogy/vorpal/master/Vorpal/AggregateMapper) to access the underyling [ActiveRecordRelation](https://api.rubyonrails.org/classes/ActiveRecord/Relation.html):
211
205
 
212
206
  ```ruby
213
- def find_all
214
- @mapper.query.load_all # use the mapper to load all the aggregates
207
+ def find_special_ones
208
+ # use `load_all` or `load_one` to convert from ActiveRecord objects to domain POROs.
209
+ @mapper.query.where(special: true).load_all
215
210
  end
216
211
  ```
217
212
 
@@ -233,19 +228,9 @@ For example:
233
228
 
234
229
  **A.** You can use [ActiveModel::Serialization](http://api.rubyonrails.org/classes/ActiveModel/Serialization.html) or [ActiveModel::Serializers](https://github.com/rails-api/active_model_serializers) but they are not heartily recommended. The former is too coupled to the model and the latter is too coupled to Rails controllers. Vorpal uses [SimpleSerializer](https://github.com/nulogy/simple_serializer) for this purpose.
235
230
 
236
- ## Running Tests
237
-
238
- 1. Start a PostgreSQL server.
239
- 2. Either:
240
- * Create a DB user called `vorpal` with password `pass`. OR:
241
- * Modify `spec/helpers/db_helpers.rb`.
242
- 3. Run `rake` from the terminal.
243
-
244
- ## Contributors
231
+ **Q.** Are `updated_at` and `created_at` supported?
245
232
 
246
- * [Sean Kirby](https://github.com/sskirby)
247
- * [Paul Sobocinski](https://github.com/psobocinski)
248
- * [Jason Cheong-Kee-You](https://github.com/jchunky)
233
+ **A.** Yes. If they exist on your database tables, they will behave exactly as if you were using vanilla ActiveRecord.
249
234
 
250
235
  ## Contributing
251
236
 
@@ -254,3 +239,35 @@ For example:
254
239
  3. Commit your changes (`git commit -am 'Add some feature'`)
255
240
  4. Push to the branch (`git push origin my-new-feature`)
256
241
  5. Create a new Pull Request
242
+
243
+ ## OSX Environment setup
244
+
245
+ 1. Install [Homebrew](https://brew.sh/)
246
+ 2. Install [rbenv](https://github.com/rbenv/rbenv#installation) ([RVM](https://rvm.io/) can work too)
247
+ 3. Install [DirEnv](https://direnv.net/docs/installation.html) (`brew install direnv`)
248
+ 4. Install Docker Desktop Community Edition (`brew cask install docker`)
249
+ 5. Start Docker Desktop Community Edition (`CMD+space docker ENTER`)
250
+ 6. Install Ruby (`rbenv install 2.7.0`)
251
+ 7. Install PostgreSQL (`brew install postgresql`)
252
+ 8. Clone the repo (`git clone git@github.com:nulogy/vorpal.git`) and `cd` to the project root.
253
+ 8. Copy the contents of `gemfiles/rails_<version>.gemfile.lock` into a `Gemfile.lock` file
254
+ at the root of the project. (`cp gemfiles/rails_6_0.gemfile.lock gemfile.lock`)
255
+ 9. `bundle`
256
+
257
+ ### Running Tests
258
+
259
+ 1. Start a PostgreSQL server using `docker-compose up`
260
+ 3. Run `rake` from the terminal to run all specs or `rspec <path to spec file>` to
261
+ run a single spec.
262
+
263
+ ### Running Tests for a specific version of Rails
264
+
265
+ 1. Start a PostgreSQL server using `docker-compose up`
266
+ 2. Run `appraisal rails-5-2 rake` from the terminal to run all specs or
267
+ `appraisal rails-5-2 rspec <path to spec file>` to run a single spec.
268
+
269
+ Please see the [Appraisal gem docs](https://github.com/thoughtbot/appraisal) for more information.
270
+
271
+ ## Contributors
272
+
273
+ See who's [contributed](https://github.com/nulogy/vorpal/graphs/contributors)!
@@ -84,6 +84,17 @@ module Vorpal
84
84
  @engine
85
85
  end
86
86
 
87
+ # Returns a 'Vorpal-aware' [ActiveRecord::Relation](https://api.rubyonrails.org/classes/ActiveRecord/Relation.html)
88
+ # for the ActiveRecord object underlying the domain entity mapped by this mapper.
89
+ #
90
+ # This method allows you to easily access the power of ActiveRecord::Relation to do more complex
91
+ # queries in your repositories.
92
+ #
93
+ # The ActiveRecord::Relation is 'Vorpal-aware' because it has the {#load_one} and {#load_many} methods
94
+ # mixed in so that you can get the POROs from your domain model instead of the ActiveRecord
95
+ # objects normally returned by ActiveRecord::Relation.
96
+ #
97
+ # @return [ActiveRecord::Relation]
87
98
  def query
88
99
  @engine.query(@domain_class)
89
100
  end
@@ -1,6 +1,5 @@
1
1
  require 'vorpal/loaded_objects'
2
2
  require 'vorpal/util/array_hash'
3
- require 'vorpal/db_driver'
4
3
 
5
4
  module Vorpal
6
5
  # Handles loading of objects from the database.
@@ -13,7 +12,7 @@ module Vorpal
13
12
  end
14
13
 
15
14
  def load_from_db(ids, config)
16
- db_roots = @db_driver.load_by_id(config, ids)
15
+ db_roots = @db_driver.load_by_id(config.db_class, ids)
17
16
  load_from_db_objects(db_roots, config)
18
17
  end
19
18
 
@@ -123,7 +122,7 @@ module Vorpal
123
122
 
124
123
  def load_all(db_driver)
125
124
  return [] if @ids.empty?
126
- db_driver.load_by_id(@config, @ids)
125
+ db_driver.load_by_id(@config.db_class, @ids)
127
126
  end
128
127
  end
129
128
 
@@ -138,7 +137,7 @@ module Vorpal
138
137
 
139
138
  def load_all(db_driver)
140
139
  return [] if @fk_values.empty?
141
- db_driver.load_by_foreign_key(@config, @fk_values, @fk_info)
140
+ db_driver.load_by_foreign_key(@config.db_class, @fk_values, @fk_info)
142
141
  end
143
142
  end
144
143
  end
@@ -0,0 +1,181 @@
1
+ require 'vorpal/util/string_utils.rb'
2
+
3
+ module Vorpal
4
+ module Driver
5
+ # Interface between the database and Vorpal for PostgreSQL using ActiveRecord.
6
+ class Postgresql
7
+ def initialize
8
+ @sequence_names = {}
9
+ end
10
+
11
+ def insert(db_class, db_objects)
12
+ if ActiveRecord::VERSION::MAJOR >= 6
13
+ return if db_objects.empty?
14
+
15
+ update_timestamps_on_create(db_class, db_objects)
16
+ db_class.insert_all!(db_objects.map(&:attributes))
17
+ elsif defined? ActiveRecord::Import
18
+ db_class.import(db_objects, validate: false)
19
+ else
20
+ db_objects.each do |db_object|
21
+ db_object.save!(validate: false)
22
+ end
23
+ end
24
+ end
25
+
26
+ def update(db_class, db_objects)
27
+ if ActiveRecord::VERSION::MAJOR >= 6
28
+ return if db_objects.empty?
29
+
30
+ update_timestamps_on_update(db_class, db_objects)
31
+ db_class.upsert_all(db_objects.map(&:attributes))
32
+ else
33
+ db_objects.each do |db_object|
34
+ db_object.save!(validate: false)
35
+ end
36
+ end
37
+ end
38
+
39
+ def destroy(db_class, ids)
40
+ db_class.where(id: ids).delete_all
41
+ end
42
+
43
+ # Loads instances of the given class by primary key.
44
+ #
45
+ # @param db_class [Class] A subclass of ActiveRecord::Base
46
+ # @return [[Object]] An array of entities.
47
+ def load_by_id(db_class, ids)
48
+ db_class.where(id: ids).to_a
49
+ end
50
+
51
+ # Loads instances of the given class whose foreign key has the given value.
52
+ #
53
+ # @param db_class [Class] A subclass of ActiveRecord::Base
54
+ # @param id [Integer] The value of the foreign key to find by. (Can also be an array of ids.)
55
+ # @param foreign_key_info [ForeignKeyInfo] Meta data for the foreign key.
56
+ # @return [[Object]] An array of entities.
57
+ def load_by_foreign_key(db_class, id, foreign_key_info)
58
+ arel = db_class.where(foreign_key_info.fk_column => id)
59
+ arel = arel.where(foreign_key_info.fk_type_column => foreign_key_info.fk_type) if foreign_key_info.polymorphic?
60
+ arel.to_a
61
+ end
62
+
63
+ # Fetches primary key values to be used for new entities.
64
+ #
65
+ # @param db_class [Class] A subclass of ActiveRecord::Base
66
+ # @return [[Integer]] An array of unused primary keys.
67
+ def get_primary_keys(db_class, count)
68
+ result = execute("select nextval($1) from generate_series(1,$2);", [sequence_name(db_class), count])
69
+ result.rows.map(&:first).map(&:to_i)
70
+ end
71
+
72
+ # Builds an ORM Class for accessing data in the given DB table.
73
+ #
74
+ # @param model_class [Class] The PORO class that we are creating a DB interface class for.
75
+ # @param table_name [String] Name of the DB table the DB class should interface with.
76
+ # @return [Class] ActiveRecord::Base Class
77
+ def build_db_class(model_class, table_name)
78
+ db_class = Class.new(ActiveRecord::Base) do
79
+ class << self
80
+ # This is overridden for two reasons:
81
+ # 1) For anonymous classes, #name normally returns nil. Class names in Ruby come from the
82
+ # name of the constant they are assigned to.
83
+ # 2) Because the default implementation for Class#name for anonymous classes is very, very
84
+ # slow. https://bugs.ruby-lang.org/issues/11119
85
+ # Remove this override once #2 has been fixed!
86
+ def name
87
+ @name ||= "Vorpal_generated_ActiveRecord__Base_class_for_#{vorpal_model_class_name}"
88
+ end
89
+
90
+ # Overridden because, like #name, the default implementation for anonymous classes is very,
91
+ # very slow.
92
+ def to_s
93
+ name
94
+ end
95
+
96
+ attr_accessor :vorpal_model_class_name
97
+ end
98
+ end
99
+
100
+ db_class.vorpal_model_class_name = Util::StringUtils.escape_class_name(model_class.name)
101
+ db_class.table_name = table_name
102
+ db_class
103
+ end
104
+
105
+ # Builds a composable query object (e.g. ActiveRecord::Relation) with Vorpal methods mixed in
106
+ # for querying for instances of the given AR::Base class.
107
+ #
108
+ # @param db_class [Class] A subclass of ActiveRecord::Base
109
+ def query(db_class, aggregate_mapper)
110
+ db_class.unscoped.extending(ArelQueryMethods.new(aggregate_mapper))
111
+ end
112
+
113
+ private
114
+
115
+ # Adapted from https://github.com/rails/rails/blob/614580270d7789e5275defc3da020ce27b3b2302/activerecord/lib/active_record/timestamp.rb#L99
116
+ def update_timestamps_on_create(db_class, db_objects)
117
+ return unless db_class.record_timestamps
118
+
119
+ current_time = db_class.current_time_from_proper_timezone
120
+ db_objects.each do |db_object|
121
+ db_class.all_timestamp_attributes_in_model.each do |column|
122
+ db_object.write_attribute(column, current_time) unless db_object.read_attribute(column)
123
+ end
124
+ end
125
+ end
126
+
127
+ #Adapted from https://github.com/rails/rails/blob/614580270d7789e5275defc3da020ce27b3b2302/activerecord/lib/active_record/timestamp.rb#L111
128
+ def update_timestamps_on_update(db_class, db_objects)
129
+ return unless db_class.record_timestamps
130
+
131
+ current_time = db_class.current_time_from_proper_timezone
132
+ db_objects.each do |db_object|
133
+ db_class.timestamp_attributes_for_update_in_model.each do |column|
134
+ db_object.write_attribute(column, current_time)
135
+ end
136
+ end
137
+ end
138
+
139
+ def sequence_name(db_class)
140
+ @sequence_names[db_class] ||= execute(
141
+ "SELECT substring(column_default from '''(.*)''') FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = $1 AND column_name = 'id' LIMIT 1",
142
+ [db_class.table_name]
143
+ ).rows.first.first
144
+ end
145
+
146
+ def execute(sql, binds)
147
+ binds = binds.map { |row| [nil, row] }
148
+ ActiveRecord::Base.connection.exec_query(sql, 'SQL', binds)
149
+ end
150
+ end
151
+
152
+ class ArelQueryMethods < Module
153
+ def initialize(mapper)
154
+ @mapper = mapper
155
+ end
156
+
157
+ def extended(descendant)
158
+ super
159
+ descendant.extend(Methods)
160
+ descendant.vorpal_aggregate_mapper = @mapper
161
+ end
162
+
163
+ # Methods in this module will appear on any composable
164
+ module Methods
165
+ attr_writer :vorpal_aggregate_mapper
166
+
167
+ # See {AggregateMapper#load_many}.
168
+ def load_many
169
+ db_roots = self.to_a
170
+ @vorpal_aggregate_mapper.load_many(db_roots)
171
+ end
172
+
173
+ # See {AggregateMapper#load_one}.
174
+ def load_one
175
+ db_root = self.first
176
+ @vorpal_aggregate_mapper.load_one(db_root)
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -22,43 +22,58 @@ module Vorpal
22
22
  @attributes.concat(attributes)
23
23
  end
24
24
 
25
- # Defines a one-to-many association with a list of objects of the same type.
25
+ # Defines a one-to-many association to another type where the foreign key is stored on the child.
26
26
  #
27
- # @param name [String] Name of the attribute that will refer to the other object.
27
+ # In Object-Oriented programming, associations are *directed*. This means that they can only be
28
+ # traversed in one direction: from the type that defines the association (the one with the
29
+ # getter) to the type that is associated. They end that defines the association is called the
30
+ # 'Parent' and the end that is associated is called the 'Child'.
31
+ #
32
+ # @param name [String] Name of the association getter.
28
33
  # @param options [Hash]
29
- # @option options [Boolean] :owned
30
- # @option options [String] :fk
31
- # @option options [String] :fk_type
32
- # @option options [Class] :child_class
34
+ # @option options [Boolean] :owned (True) True if the child type belongs to the aggregate. Changes to any object belonging to the aggregate will be persisted when the aggregate is persisted.
35
+ # @option options [String] :fk (Parent class name converted to snakecase and appended with a '_id') The name of the DB column on the child that contains the foreign key reference to the parent.
36
+ # @option options [String] :fk_type The name of the DB column on the child that contains the parent class name. Only needed when there is an association from the child side that is polymorphic.
37
+ # @option options [Class] :child_class (name converted to a Class) The child class.
33
38
  def has_many(name, options={})
34
39
  @has_manys << {name: name}.merge(options)
35
40
  end
36
41
 
37
- # Defines a one-to-one association with another object where the foreign key
38
- # is stored on the other object.
42
+ # Defines a one-to-one association to another type where the foreign key
43
+ # is stored on the child.
44
+ #
45
+ # In Object-Oriented programming, associations are *directed*. This means that they can only be
46
+ # traversed in one direction: from the type that defines the association (the one with the
47
+ # getter) to the type that is associated. They end that defines the association is called the
48
+ # 'Parent' and the end that is associated is called the 'Child'.
39
49
  #
40
- # @param name [String] Name of the attribute that will refer to the other object.
50
+ # @param name [String] Name of the association getter.
41
51
  # @param options [Hash]
42
- # @option options [Boolean] :owned
43
- # @option options [String] :fk
44
- # @option options [String] :fk_type
45
- # @option options [Class] :child_class
52
+ # @option options [Boolean] :owned (True) True if the child type belongs to the aggregate. Changes to any object belonging to the aggregate will be persisted when the aggregate is persisted.
53
+ # @option options [String] :fk (Parent class name converted to snakecase and appended with a '_id') The name of the DB column on the child that contains the foreign key reference to the parent.
54
+ # @option options [String] :fk_type The name of the DB column on the child that contains the parent class name. Only needed when there is an association from the child side that is polymorphic.
55
+ # @option options [Class] :child_class (name converted to a Class) The child class.
46
56
  def has_one(name, options={})
47
57
  @has_ones << {name: name}.merge(options)
48
58
  end
49
59
 
50
- # Defines a one-to-one association with another object where the foreign key
51
- # is stored on this object.
60
+ # Defines a one-to-one association with another type where the foreign key
61
+ # is stored on the parent.
62
+ #
63
+ # This association can be polymorphic. I.E. children can be of different types.
52
64
  #
53
- # This association can be polymorphic. i.e.
65
+ # In Object-Oriented programming, associations are *directed*. This means that they can only be
66
+ # traversed in one direction: from the type that defines the association (the one with the
67
+ # getter) to the type that is associated. They end that defines the association is called the
68
+ # 'Parent' and the end that is associated is called the 'Child'.
54
69
  #
55
- # @param name [String] Name of the attribute that will refer to the other object.
70
+ # @param name [String] Name of the association getter.
56
71
  # @param options [Hash]
57
- # @option options [Boolean] :owned
58
- # @option options [String] :fk
59
- # @option options [String] :fk_type
60
- # @option options [Class] :child_class
61
- # @option options [[Class]] :child_classes
72
+ # @option options [Boolean] :owned (True) True if the child type belongs to the aggregate. Changes to any object belonging to the aggregate will be persisted when the aggregate is persisted.
73
+ # @option options [String] :fk (Child class name converted to snakecase and appended with a '_id') The name of the DB column on the parent that contains the foreign key reference to the child.
74
+ # @option options [String] :fk_type The name of the DB column on the parent that contains the child class name. Only needed when the association is polymorphic.
75
+ # @option options [Class] :child_class (name converted to a Class) The child class.
76
+ # @option options [[Class]] :child_classes The list of possible classes that can be children. This is for polymorphic associations. Takes precedence over `:child_class`.
62
77
  def belongs_to(name, options={})
63
78
  @belongs_tos << {name: name}.merge(options)
64
79
  end