objectid_columns 1.0.0
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 +18 -0
- data/.travis.yml +35 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +224 -0
- data/Rakefile +6 -0
- data/lib/objectid_columns/active_record/base.rb +33 -0
- data/lib/objectid_columns/active_record/relation.rb +40 -0
- data/lib/objectid_columns/dynamic_methods_module.rb +104 -0
- data/lib/objectid_columns/extensions.rb +40 -0
- data/lib/objectid_columns/has_objectid_columns.rb +47 -0
- data/lib/objectid_columns/objectid_columns_manager.rb +298 -0
- data/lib/objectid_columns/version.rb +4 -0
- data/lib/objectid_columns.rb +121 -0
- data/objectid_columns.gemspec +51 -0
- data/spec/objectid_columns/helpers/database_helper.rb +178 -0
- data/spec/objectid_columns/helpers/system_helpers.rb +90 -0
- data/spec/objectid_columns/system/basic_system_spec.rb +398 -0
- data/spec/objectid_columns/system/extensions_spec.rb +69 -0
- metadata +145 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA512:
|
3
|
+
metadata.gz: 8752dd26c637b7ba079438edd6abedce5c7df3fe44965c176f3aa3b3c137b1fbd504d5193e69e6d6ac200d445fe1e2808b04d494395c304b92c0b6878838f43e
|
4
|
+
data.tar.gz: aecea712ee39f11a59e245bce3d89136415b3429317500f4b8087c8e3b90b53ca6df937b904ca7f0abfc328f6961016105422ca94a6b4bfd99ad98644758e472
|
5
|
+
SHA1:
|
6
|
+
metadata.gz: a55261b9a4c1cdf209e6b14c9d04a5a8aa0ff38d
|
7
|
+
data.tar.gz: fc27e32b0629998e0aa28180ba01cbb71ae1fc29
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
rvm:
|
2
|
+
- "1.8.7"
|
3
|
+
- "1.9.3"
|
4
|
+
- "2.0.0"
|
5
|
+
- "2.1.0"
|
6
|
+
- "jruby-1.7.9"
|
7
|
+
env:
|
8
|
+
- OBJECTID_COLUMNS_AR_TEST_VERSION=3.0.20 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=mysql
|
9
|
+
- OBJECTID_COLUMNS_AR_TEST_VERSION=3.0.20 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=postgres
|
10
|
+
- OBJECTID_COLUMNS_AR_TEST_VERSION=3.0.20 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=sqlite
|
11
|
+
- OBJECTID_COLUMNS_AR_TEST_VERSION=3.1.12 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=mysql
|
12
|
+
- OBJECTID_COLUMNS_AR_TEST_VERSION=3.1.12 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=postgres
|
13
|
+
- OBJECTID_COLUMNS_AR_TEST_VERSION=3.1.12 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=sqlite
|
14
|
+
- OBJECTID_COLUMNS_AR_TEST_VERSION=3.2.16 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=mysql
|
15
|
+
- OBJECTID_COLUMNS_AR_TEST_VERSION=3.2.16 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=postgres
|
16
|
+
- OBJECTID_COLUMNS_AR_TEST_VERSION=3.2.16 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=sqlite
|
17
|
+
- OBJECTID_COLUMNS_AR_TEST_VERSION=4.0.2 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=mysql
|
18
|
+
- OBJECTID_COLUMNS_AR_TEST_VERSION=4.0.2 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=postgres
|
19
|
+
- OBJECTID_COLUMNS_AR_TEST_VERSION=4.0.2 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=sqlite
|
20
|
+
before_script:
|
21
|
+
- mysql -e 'create database myapp_test;'
|
22
|
+
- psql -c 'create database myapp_test;' -U postgres
|
23
|
+
matrix:
|
24
|
+
exclude:
|
25
|
+
# ActiveRecord 4.x doesn't support Ruby 1.8.7
|
26
|
+
- rvm: 1.8.7
|
27
|
+
env: OBJECTID_COLUMNS_AR_TEST_VERSION=4.0.2 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=mysql
|
28
|
+
- rvm: 1.8.7
|
29
|
+
env: OBJECTID_COLUMNS_AR_TEST_VERSION=4.0.2 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=postgres
|
30
|
+
- rvm: 1.8.7
|
31
|
+
env: OBJECTID_COLUMNS_AR_TEST_VERSION=4.0.2 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=sqlite
|
32
|
+
allow_failures:
|
33
|
+
- env: OBJECTID_COLUMNS_AR_TEST_VERSION=3.1.12 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=sqlite
|
34
|
+
- rvm: 1.8.7
|
35
|
+
env: OBJECTID_COLUMNS_AR_TEST_VERSION=3.2.16 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=sqlite
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Swiftype, Inc.
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,224 @@
|
|
1
|
+
# ObjectidColumns
|
2
|
+
|
3
|
+
Transparely store MongoDB [ObjectId](http://docs.mongodb.org/manual/reference/object-id/) values (the primary key of
|
4
|
+
all MongoDB tables) in ActiveRecord objects in a relational database. You can store values as:
|
5
|
+
|
6
|
+
* A binary column (`BINARY`, `VARBINARY`, `BLOB`, _etc._) of length at least 12; the ObjectId value will be stored as
|
7
|
+
pure binary data — the most efficient format. (Most databases, including MySQL and PostgreSQL, can index this
|
8
|
+
column and work with it just like a String; it just takes half as much space (!).)
|
9
|
+
* A String column (`CHAR`, `VARCHAR`, _etc._) of length at least 24; the ObjectId value will be stored as a hexadecimal
|
10
|
+
string.
|
11
|
+
|
12
|
+
(Note that it is not possible to store binary data transparently in a String column, because not all byte sequences
|
13
|
+
are valid binary data in all possible character sets.)
|
14
|
+
|
15
|
+
Once declared, an ObjectId column will return instances of either `BSON::ObjectId` or `Moped::BSON::ObjectId`
|
16
|
+
(depending on which one you have loaded) when you access an attribute of a model that you've declared as an ObjectId
|
17
|
+
column. It will accept a String (in either hex or binary formats) or an instance of either of those classes when
|
18
|
+
assigning to the column.
|
19
|
+
|
20
|
+
`ObjectidColumns` also allows you to use ObjectId values as primary keys; it will assign them to new records by
|
21
|
+
default, and make sure `find(id)` accepts them.
|
22
|
+
|
23
|
+
This gem requires either the `moped` gem (which defines `Moped::BSON::ObjectId`) or the `bson` gem (which defines
|
24
|
+
`BSON::ObjectId`) for the actual ObjectId classes it uses. It declares an official dependency on neither, because we
|
25
|
+
want to allow you to use either one. It will accept either one when assigning ObjectIds; it will return ObjectIds as
|
26
|
+
whichever one you have loaded, (currently) preferring `BSON::ObjectId` if you have both.
|
27
|
+
|
28
|
+
ObjectidColumns supports Ruby 1.8.7, 1.9.3, 2.0.0, and 2.1.0, plus JRuby 1.7.9; it supports ActiveRecord 3.0.20,
|
29
|
+
3.1.12, 3.2.16, and 4.0.2. It supports SQLite 3.x, MySQL 5.x, and PostgreSQL 8.x. (These are just the versions it's
|
30
|
+
tested against; while it will not work with ActiveRecord 2.x, it is otherwise highly unlikely to be sensitive to
|
31
|
+
exact ActiveRecord or Ruby versions, or type of RDBMS, and generally should work with most combinations.)
|
32
|
+
|
33
|
+
*Note*: If you use SQLite3 with ActiveRecord 3.1.x on MRI (_i.e._, not JRuby), or SQLite3 with ActiveRecord 3.2.x on
|
34
|
+
MRI 1.8.7, there is a bug in the SQLite3-ActiveRecord integration that causes ObjectId primary keys to not work. (They
|
35
|
+
are inserted properly, but searching by primary key fails.) Needless to say, these are very rare combinations of
|
36
|
+
software to be running in production, but it's worth noting. (ActiveRecord 3.0.x or 3.2.x work fine with SQLite3 on
|
37
|
+
MRI; SQLite3 with ActiveRecord 3.2.x works fine on MRI >= 1.9.3; and MySQL and PostgreSQL work perfectly everywhere.)
|
38
|
+
|
39
|
+
Current build status: 
|
40
|
+
|
41
|
+
Brought to you by the folks at [Swiftype](https://www.swiftype.com). First version written by [Andrew Geweke](https://www.github.com/ageweke).
|
42
|
+
|
43
|
+
## Installation
|
44
|
+
|
45
|
+
First, make sure you have either `BSON::ObjectId` or `Moped::BSON::ObjectId` defined in your Rails environment;
|
46
|
+
these are the two ObjectId classes that `ObjectidColumns` can work with. If you don't have either defined, add one
|
47
|
+
of these lines to your `Gemfile` (loading both is fine, but unnecessary):
|
48
|
+
|
49
|
+
gem 'bson'
|
50
|
+
OR gem 'moped'
|
51
|
+
|
52
|
+
Now, add this line to your application's Gemfile:
|
53
|
+
|
54
|
+
gem 'objectid_columns'
|
55
|
+
|
56
|
+
And then execute:
|
57
|
+
|
58
|
+
$ bundle
|
59
|
+
|
60
|
+
Or install it yourself as:
|
61
|
+
|
62
|
+
$ gem install objectid_columns
|
63
|
+
|
64
|
+
## Usage
|
65
|
+
|
66
|
+
If you name your object-ID columns with `_oid` at the end, simply do this:
|
67
|
+
|
68
|
+
class MyModel < ActiveRecord::Base
|
69
|
+
has_objectid_columns
|
70
|
+
end
|
71
|
+
|
72
|
+
This will automatically find any columns that end in `_oid` and make them ObjectId columns. When reading them, you will
|
73
|
+
get back an instance of an ObjectId class (from `moped` or `bson`, depending on which one you have loaded; see the
|
74
|
+
introduction for more). When writing them, you can assign a String in hex or binary formats, or an instance of either
|
75
|
+
of the supported ObjectId classes.
|
76
|
+
|
77
|
+
If you didn't name your columns this way, or _don't_ want to pick up columns ending in `_oid`, just name them
|
78
|
+
explicitly:
|
79
|
+
|
80
|
+
class MyModel < ActiveRecord::Base
|
81
|
+
has_objectid_columns :some_oid, :foo
|
82
|
+
end
|
83
|
+
|
84
|
+
This will not only define `some_oid` and `foo` as being ObjectId columns, but it will also skip the automatic detection
|
85
|
+
of columns ending in `_oid`.
|
86
|
+
|
87
|
+
ObjectidColumns will never automatically make a primary-key column an ObjectId, even if it ends with `_oid`; if you
|
88
|
+
want a primary key to be an ObjectId, you must do that explicitly, using `has_objectid_primary_key`, below.
|
89
|
+
|
90
|
+
Note that trying to declare a column as an ObjectId column if it isn't of a supported type (a type that ActiveRecord
|
91
|
+
considers to be `:string` or `:binary`), or if it isn't long enough to support an ObjectId (twelve characters for
|
92
|
+
binary columns, 24 for string columns); this will happen from the `has_objectid_columns` call (so at load time for the
|
93
|
+
model class).
|
94
|
+
|
95
|
+
Once you have declared such a column:
|
96
|
+
|
97
|
+
my_model = MyModel.find(...)
|
98
|
+
|
99
|
+
my_model.my_oid # => BSON::ObjectId('52eab2cf78161f1314000001')
|
100
|
+
my_model.my_oid.to_s # => "52eab2cf78161f1314000001" (built-in behavior from BSON::ObjectId)
|
101
|
+
my_model.my_oid.to_binary # => "R\xEA\xB2\xCFx\x16\x1F\x13\x14\x00\x00\x01"
|
102
|
+
my_model.my_oid.to_binary.encoding # => #<Encoding:ASCII-8BIT>
|
103
|
+
|
104
|
+
my_model.my_oid = BSON::ObjectId.new # OK
|
105
|
+
my_model.my_oid = "52eab32878161f1314000002" # OK
|
106
|
+
my_model.my_oid = "R\xEA\xB2\xCFx\x16\x1F\x13\x14\x00\x00\x01" # OK
|
107
|
+
|
108
|
+
MyModel.where(:my_oid => some_oid).first # => my_model -- i.e., where(...) works with a hash.
|
109
|
+
|
110
|
+
Note that to assign a binary-format string, it must have an encoding of `Encoding::BINARY` (which is an alias for
|
111
|
+
`Encoding::ASCII-8BIT`). (If your string has a different encoding, it may be coming from a source that does not
|
112
|
+
actually support full binary data transparently, which _will_ cause big problems.)
|
113
|
+
|
114
|
+
### Gotchas
|
115
|
+
|
116
|
+
If you query on an ObjectId column and use the hash syntax — _i.e._, `MyModel.where(:some_oid => ...)` —
|
117
|
+
then everything will work perfectly; `ObjectidColumns` looks for this syntax and properly translates the value from
|
118
|
+
an ObjectId object, binary String, or hex String to the proper database value. On the other hand, if you specify a
|
119
|
+
SQL statement or fragment of SQL yourself, you must do the translation, or you'll either get a database error or just
|
120
|
+
no rows found:
|
121
|
+
|
122
|
+
MyModel.where("some_oid = ?", my_oid.to_bson_id.to_binary) # if some_oid is binary
|
123
|
+
MyModel.where("some_oid = ?", my_oid.to_bson_id.to_s) # if some_oid is a String
|
124
|
+
|
125
|
+
(Without actively parsing SQL, which is kind of an insane thing to do, there is no easy way around this. Even if we
|
126
|
+
detected ObjectId objects in the set of values passed in, we'd have no way of figuring out which ObjectId column they
|
127
|
+
were constraining on, and thus whether to turn them into binary or hexadecimal ObjectId values.)
|
128
|
+
|
129
|
+
### Using an ObjectId for a Primary Key
|
130
|
+
|
131
|
+
You can use ObjectId values as primary keys:
|
132
|
+
|
133
|
+
class MyModel < ActiveRecord::Base
|
134
|
+
has_objectid_primary_key
|
135
|
+
end
|
136
|
+
|
137
|
+
Perhaps obviously, your primary-key column must either be `:binary` of length >= 12 or `:string` of length >= 24.
|
138
|
+
|
139
|
+
If your primary-key column is not called `:id`, you must do one of two things:
|
140
|
+
|
141
|
+
* Set the primary key on the class _before_ you call `has_objectid_primary_key`, by just using the normal ActiveRecord
|
142
|
+
`self.primary_key = :foo` syntax.
|
143
|
+
* Pass the primary key as an argument to `has_objectid_primary_key`: `has_objectid_primary_key :foo`.
|
144
|
+
|
145
|
+
(The two are exactly equivalent.)
|
146
|
+
|
147
|
+
When you do this, `ObjectidColumns` adds a `before_create` hook to your model, assigning a new ObjectId immediately
|
148
|
+
before the first time a row is saved in the database. This will not replace an existing ID, so, if you always (or
|
149
|
+
sometimes) assign a new ID yourself, `ObjectidColumns` will not overwrite your ID.
|
150
|
+
|
151
|
+
Perhaps obviously, this means that new IDs are generated and stored client-side; you almost certainly should declare
|
152
|
+
your primary-key column as `NOT NULL`, but you don't need to give it a default (and probably shouldn't, unless you're
|
153
|
+
using a database function that is capable of generating correctly-formatted ObjectId values using the correct
|
154
|
+
algorithm).
|
155
|
+
|
156
|
+
|
157
|
+
### Setting the Preferred Class
|
158
|
+
|
159
|
+
If you have both the `bson` and `moped` gems defined, then, by default, ObjectId columns will be returned as instances
|
160
|
+
of `bson`'s `BSON::ObjectId` class. If you want to use `moped`'s instead, do this:
|
161
|
+
|
162
|
+
ObjectidColumns.preferred_bson_class = Moped::BSON::ObjectId
|
163
|
+
|
164
|
+
### Extensions
|
165
|
+
|
166
|
+
This gem extends String with a single method, `#to_bson_id`; it simply returns an instance of the preferred BSON class
|
167
|
+
from that String if it's in either the valid hex or the valid binary format, or raises `ArgumentError` otherwise.
|
168
|
+
|
169
|
+
This gem also extends whatever BSON ObjectId classes are loaded with methods `to_bson_id` (which just returns `self`),
|
170
|
+
and the method `to_binary`, which returns a binary String of length 12 for that object ID.
|
171
|
+
|
172
|
+
### Running Specs
|
173
|
+
|
174
|
+
`objectid_columns` has thorough system-level (_i.e._, integration) tests, written in RSpec. Because nearly all of its
|
175
|
+
functionality is centered around interfacing with ActiveRecord (as opposed to having significant, complex code within
|
176
|
+
its codebase directly), there are no unit tests — they would simply be setting complex expectations around calls
|
177
|
+
to ActiveRecord, making the tests fragile and not particularly useful.
|
178
|
+
|
179
|
+
In order to run these specs, you must have access to a database you can use. It's best if the database is dedicated
|
180
|
+
to running these specs. The tests create and destroy their own tables, and make every effort to clean up anything they
|
181
|
+
created at the end — so it should be possible to piggyback on top of an existing database you also use for other
|
182
|
+
things. However, it's _always_ much safer to use a dedicated database. (Note that this is intentional use of the word
|
183
|
+
"database", as opposed to "database server"; you don't need, for example, an entirely separate instance of `mysqld`
|
184
|
+
— just a separate database that you can switch to using `USE ....`.)
|
185
|
+
|
186
|
+
Once you have this set up, simply create a file at the root level of the Gem (_i.e._, inside the root
|
187
|
+
`objectid_columns` directory) called `spec_database_config.rb`, and define a constant
|
188
|
+
`OBJECTID_COLUMNS_SPEC_DATABASE_CONFIG` as so:
|
189
|
+
|
190
|
+
OBJECTID_COLUMNS_SPEC_DATABASE_CONFIG = {
|
191
|
+
:database_gem_name => 'mysql2',
|
192
|
+
:require => 'mysql2',
|
193
|
+
:config => {
|
194
|
+
:adapter => 'mysql2',
|
195
|
+
:database => 'objectid_columns_specs_db',
|
196
|
+
:username => 'root'
|
197
|
+
}
|
198
|
+
}
|
199
|
+
|
200
|
+
The keys are as follows:
|
201
|
+
|
202
|
+
* `:database_gem_name` is the name of the RubyGem that provides access to the database — exactly as you'd put
|
203
|
+
it in a `Gemfile`;
|
204
|
+
* `:require` is whatever should be passed to Ruby's built-in `require` statement to require the Gem —
|
205
|
+
typically this is the same as `:database_gem_name`, but not always (this is the same as a Gemfile's `:require => ..
|
206
|
+
` syntax);
|
207
|
+
* `:config` is exactly what gets passed to `ActiveRecord::Base.establish_connection`, and so you can pass any
|
208
|
+
options that it accepts (which are the same as what goes in Rails' `database.yml`).
|
209
|
+
|
210
|
+
Once you've done this, you can run the system specs using `bundle exec rspec spec/objectid_columns/system`. (Or run
|
211
|
+
them along with the unit specs with a simple `bundle exec rspec spec`.)
|
212
|
+
|
213
|
+
Note that there's also support deep in the code (in
|
214
|
+
`objectid_columns/spec/objectid_columns/helpers/database_helper.rb`)
|
215
|
+
for defining connections to [Travis CI](https://travis-ci.org/)'s database options, so that Travis can run the tests
|
216
|
+
automatically. Generally, you don't need to worry about this, but it's worth noting.
|
217
|
+
|
218
|
+
## Contributing
|
219
|
+
|
220
|
+
1. Fork it ( http://github.com/swiftype/objectid_columns/fork )
|
221
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
222
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
223
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
224
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'objectid_columns'
|
2
|
+
require 'active_support'
|
3
|
+
require 'objectid_columns/has_objectid_columns'
|
4
|
+
|
5
|
+
module ObjectidColumns
|
6
|
+
module ActiveRecord
|
7
|
+
# This module gets included into ActiveRecord::Base when ObjectidColumns loads. It is just a "trampoline" -- the
|
8
|
+
# first time you call one of its methods, it includes ObjectidColumns::HasObjectidColumns into your model, and
|
9
|
+
# then re-calls the method. (This looks like infinite recursion, but isn't, because once we include the module,
|
10
|
+
# its implementation takes precedence over ours -- because we will always be a module earlier on the inheritance
|
11
|
+
# chain, since we by definition were included before ObjectidColumns::HasObjectidColumns.)
|
12
|
+
module Base
|
13
|
+
extend ActiveSupport::Concern
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
# Do we have any ObjectId columns? This is always false -- once we include ObjectidColumns::HasObjectidColumns,
|
17
|
+
# its implementation (which just returns 'true') takes precedence.
|
18
|
+
def has_objectid_columns?
|
19
|
+
false
|
20
|
+
end
|
21
|
+
|
22
|
+
# These are our "trampoline" methods -- the methods that you should be able to call on an ActiveRecord class
|
23
|
+
# that has never had any ObjectidColumns-related methods called on it before, that bootstrap the process.
|
24
|
+
[ :has_objectid_columns, :has_objectid_column, :has_objectid_primary_key ].each do |method_name|
|
25
|
+
define_method(method_name) do |*args|
|
26
|
+
include ::ObjectidColumns::HasObjectidColumns
|
27
|
+
send(method_name, *args)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'objectid_columns'
|
2
|
+
|
3
|
+
module ObjectidColumns
|
4
|
+
module ActiveRecord
|
5
|
+
# This module gets included into ActiveRecord::Relation; it is responsible for modifying the behavior of +where+.
|
6
|
+
# Note that when you call +where+ directly on an ActiveRecord class, it (through various AR magic) ends up calling
|
7
|
+
# ActiveRecord::Relation#where, so this takes care of that, too.
|
8
|
+
module Relation
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
# There is really only one case where we can transparently modify queries -- where you're using hash syntax:
|
12
|
+
#
|
13
|
+
# model.where(:foo_oid => <objectID>)
|
14
|
+
#
|
15
|
+
# If you're using SQL string syntax:
|
16
|
+
#
|
17
|
+
# model.where("foo_oid = ?", <objectID>)
|
18
|
+
#
|
19
|
+
# ...then there's no way to reliably determine what should be converted as an ObjectId (and, critically, to what
|
20
|
+
# format -- hex or binary -- we should convert it). As such, we leave the responsibility for that case up to
|
21
|
+
# the user.
|
22
|
+
def where(*args)
|
23
|
+
# Bail out if we don't have any ObjectId columns
|
24
|
+
return super(*args) unless respond_to?(:has_objectid_columns?) && has_objectid_columns?
|
25
|
+
# Bail out if we're not using the Hash form
|
26
|
+
return super(*args) unless args.length == 1 && args[0].kind_of?(Hash)
|
27
|
+
|
28
|
+
query = { }
|
29
|
+
args[0].each do |key, value|
|
30
|
+
# #translate_objectid_query_pair is a method defined on the ObjectidColumns::ObjectidColumnsManager, and is
|
31
|
+
# called via the delegation defined in ObjectidColumns::HasObjectidColumns.
|
32
|
+
(key, value) = translate_objectid_query_pair(key, value)
|
33
|
+
query[key] = value
|
34
|
+
end
|
35
|
+
|
36
|
+
super(query)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module ObjectidColumns
|
2
|
+
# A DynamicMethodsModule is used to add dynamically-generated methods to an existing class.
|
3
|
+
#
|
4
|
+
# Why do we need a module to do that? Why can't we simply call #define_method on the class itself?
|
5
|
+
#
|
6
|
+
# We could. However, if you do that, a few problems crop up:
|
7
|
+
#
|
8
|
+
# * There is no precendence that you can control. If you define a method +:foo+ on class Bar, then that method is
|
9
|
+
# always run when an instance of that class is sent the message +:foo+. The only way to change the behavior of
|
10
|
+
# that class is to completely redefine that method, which brings us to the second problem...
|
11
|
+
# * Overriding and +super+ doesn't work. That is, you can't override such a method and call the original method
|
12
|
+
# using +super+. You're reduced to using +alias_method_chain+, which is a mess.
|
13
|
+
# * There's no namespacing at all -- at runtime, it's not even remotely clear where these methods are coming from.
|
14
|
+
# * Finally, if you're living in a dynamic environment -- like Rails' development mode, where classes get reloaded
|
15
|
+
# very frequently -- once you define a method, it is likely to be forever defined. You have to write code to keep
|
16
|
+
# track of what you've defined, and remove it when it's no longer present.
|
17
|
+
#
|
18
|
+
# A DynamicMethodsModule fixes these problems. It's little more than a Module that lets you define methods (and
|
19
|
+
# helpfully makes #define_method +public+ to help), but it also will include itself into a target class and bind
|
20
|
+
# itself to a constant in that class (which magically gives the module a name, too). Further, it also keeps track
|
21
|
+
# of which methods you've defined, and can remove them all with #remove_all_methods!. This allows you to construct
|
22
|
+
# a much more reliable paradigm: instead of trying to figure out what methods you should remove and add when things
|
23
|
+
# change, you can just call #remove_all_methods! and then redefine whatever methods _currently_ should exist.
|
24
|
+
#
|
25
|
+
# A DynamicMethodsModule also supports class methods; if you define a method with #define_class_method, it will be
|
26
|
+
# added to a module that the target class has called +extend+ on (rather than +include+), and hence will show up as
|
27
|
+
# a class method on that class. This is useful for the exact same reasons as the base DynamicMethodsModule; it allows
|
28
|
+
# for precedence control, use of +super+, namespacing, and dynamism.
|
29
|
+
class DynamicMethodsModule < ::Module
|
30
|
+
# Creates a new instance. +target_class+ is the Class into which this module should include itself; +name+ is the
|
31
|
+
# name to which it should bind itself. (This will be bound as a constant inside that class, not at top-level on
|
32
|
+
# Object; so, for example, if +target_class+ is +User+ and +name+ is +Foo+, then this module will end up named
|
33
|
+
# +User::Foo+, not simply +Foo+.)
|
34
|
+
#
|
35
|
+
# If passed a block, the block will be evaluated in the context of this module, just like Module#new. Note that
|
36
|
+
# you <em>should not</em> use this to define methods that you want #remove_all_methods!, below, to remove; it
|
37
|
+
# won't work. Any methods you add in this block using normal +def+ will persist, even through #remove_all_methods!.
|
38
|
+
def initialize(target_class, name, &block)
|
39
|
+
raise ArgumentError, "Target class must be a Class, not: #{target_class.inspect}" unless target_class.kind_of?(Class)
|
40
|
+
raise ArgumentError, "Name must be a Symbol or String, not: #{name.inspect}" unless name.kind_of?(Symbol) || name.kind_of?(String)
|
41
|
+
|
42
|
+
@target_class = target_class
|
43
|
+
@name = name.to_sym
|
44
|
+
|
45
|
+
# Unfortunately, there appears to be no way to "un-include" a Module in Ruby -- so we have no way of replacing
|
46
|
+
# an existing DynamicMethodsModule on the target class, which is what we'd really like to do in this situation.
|
47
|
+
if @target_class.const_defined?(@name)
|
48
|
+
existing = @target_class.const_get(@name)
|
49
|
+
|
50
|
+
if existing && existing != self
|
51
|
+
raise NameError, %{You tried to define a #{self.class.name} named #{name.inspect} on class #{target_class.name},
|
52
|
+
but that class already has a constant named #{name.inspect}: #{existing.inspect}}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
@class_methods_module = Module.new
|
57
|
+
(class << @class_methods_module; self; end).send(:public, :private)
|
58
|
+
@target_class.const_set("#{@name}ClassMethods", @class_methods_module)
|
59
|
+
@target_class.send(:extend, @class_methods_module)
|
60
|
+
|
61
|
+
@target_class.const_set(@name, self)
|
62
|
+
@target_class.send(:include, self)
|
63
|
+
|
64
|
+
@methods_defined = { }
|
65
|
+
@class_methods_defined = { }
|
66
|
+
|
67
|
+
super(&block)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Removes all methods that have been defined on this module using #define_method, below. (If you use some other
|
71
|
+
# mechanism to define a method on this DynamicMethodsModule, then it will not be removed when this method is
|
72
|
+
# called.)
|
73
|
+
def remove_all_methods!
|
74
|
+
instance_methods.each do |method_name|
|
75
|
+
# Important -- we use Class#remove_method, not Class#undef_method, which does something that's different in
|
76
|
+
# some important ways.
|
77
|
+
remove_method(method_name) if @methods_defined[method_name.to_sym]
|
78
|
+
end
|
79
|
+
|
80
|
+
@class_methods_module.instance_methods.each do |method_name|
|
81
|
+
@class_methods_module.send(:remove_method, method_name) if @class_methods_defined[method_name]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Defines a method. Works identically to Module#define_method, except that it's +public+ and #remove_all_methods!
|
86
|
+
# will remove the method.
|
87
|
+
def define_method(name, &block)
|
88
|
+
name = name.to_sym
|
89
|
+
super(name, &block)
|
90
|
+
@methods_defined[name] = true
|
91
|
+
end
|
92
|
+
|
93
|
+
# Defines a class method.
|
94
|
+
def define_class_method(name, &block)
|
95
|
+
@class_methods_module.send(:define_method, name, &block)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Makes it so you can say, for example:
|
99
|
+
#
|
100
|
+
# my_dynamic_methods_module.define_method(:foo) { ... }
|
101
|
+
# my_dynamic_methods_module.private(:foo)
|
102
|
+
public :private # teehee
|
103
|
+
end
|
104
|
+
end
|