objectid_columns 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: ![Current Build Status](https://api.travis-ci.org/swiftype/objectid_columns.png?branch=master)
|
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
|