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
@@ -0,0 +1,40 @@
|
|
1
|
+
# This file adds very useful convenience methods to certain classes:
|
2
|
+
|
3
|
+
# To each of the BSON classes, we add:
|
4
|
+
#
|
5
|
+
# * #to_binary, which returns a String containing a pure-binary (12-byte) representation of the ObjectId; and
|
6
|
+
# * #to_bson_id, which simply returns +self+ -- this is so we can call this method on any object passed in where we're
|
7
|
+
# expecting an ObjectId, and, if you're already supplying an ObjectId object, it will just work.
|
8
|
+
ObjectidColumns.available_objectid_columns_bson_classes.each do |klass|
|
9
|
+
klass.class_eval do
|
10
|
+
def to_binary
|
11
|
+
[ to_s ].pack("H*")
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_bson_id
|
15
|
+
self
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# To String, we add #to_bson_id. This method knows how to convert a String that is in either the hex or pure-binary
|
21
|
+
# forms to an actual ObjectId object. Note that we use the method ObjectidColumns.construct_objectid to actually create
|
22
|
+
# the object; this way, it will follow any preference for exactly what BSON class to use that's set there.
|
23
|
+
#
|
24
|
+
# If your String is in binary form, it must have its encoding set to Encoding::BINARY (which is aliased to
|
25
|
+
# Encoding::ASCII_8BIT); if it's not in this encoding, it may well be coming from a source that doesn't support
|
26
|
+
# transparent binary data (for example, UTF-8 doesn't -- certain byte combinations are illegal in UTF-8 and will cause
|
27
|
+
# string manipulation to fail), which is a real problem.
|
28
|
+
String.class_eval do
|
29
|
+
BSON_HEX_ID_REGEX = /^[0-9a-f]{24}$/i
|
30
|
+
|
31
|
+
def to_bson_id
|
32
|
+
if self =~ BSON_HEX_ID_REGEX
|
33
|
+
ObjectidColumns.construct_objectid(self)
|
34
|
+
elsif length == 12 && ((! respond_to?(:encoding)) || (encoding == Encoding::BINARY)) # :respond_to? is for Ruby 1.8.7 support
|
35
|
+
ObjectidColumns.construct_objectid(unpack("H*").first)
|
36
|
+
else
|
37
|
+
raise ArgumentError, "#{inspect} does not seem to be a valid BSON ID; it is in neither the valid hex (exactly 24 hex characters, any encoding) nor the valid binary (12 characters, binary/ASCII-8BIT encoding) form"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/core_ext/object'
|
3
|
+
require 'objectid_columns/objectid_columns_manager'
|
4
|
+
|
5
|
+
module ObjectidColumns
|
6
|
+
# This module gets mixed into an ActiveRecord class when you say +has_objectid_column(s)+ or
|
7
|
+
# +has_objectid_primary_key+. It delegates everything it does to the ObjectidColumnsManager; we do it this way so
|
8
|
+
# that we can define all kinds of helper methods on the ObjectidColumnsManager without polluting the method
|
9
|
+
# namespace on the actual ActiveRecord class.
|
10
|
+
module HasObjectidColumns
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
|
13
|
+
# Reads the current value of the given +column_name+ (which must be an ObjectId column) as an ObjectId object.
|
14
|
+
def read_objectid_column(column_name)
|
15
|
+
self.class.objectid_columns_manager.read_objectid_column(self, column_name)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Writes a new value to the given +column_name+ (which must be an ObjectId column), accepting a String (in either
|
19
|
+
# hex or binary formats) or an ObjectId object, and transforming it to whatever storage format is correct for
|
20
|
+
# that column.
|
21
|
+
def write_objectid_column(column_name, new_value)
|
22
|
+
self.class.objectid_columns_manager.write_objectid_column(self, column_name, new_value)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Called as a +before_create+ hook, if (and only if) this class has declared +has_objectid_primary_key+ -- sets
|
26
|
+
# the primary key to a newly-generated ObjectId, unless it has one already.
|
27
|
+
def assign_objectid_primary_key
|
28
|
+
self.id ||= ObjectidColumns.new_objectid
|
29
|
+
end
|
30
|
+
|
31
|
+
module ClassMethods
|
32
|
+
# Does this class have any ObjectId columns? It does if this module has been included, since it only gets included
|
33
|
+
# if you declare an ObjectId column.
|
34
|
+
def has_objectid_columns?
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
# Delegate all of the interesting work to the ObjectidColumnsManager.
|
39
|
+
delegate :has_objectid_columns, :has_objectid_column, :has_objectid_primary_key,
|
40
|
+
:translate_objectid_query_pair, :to => :objectid_columns_manager
|
41
|
+
|
42
|
+
def objectid_columns_manager
|
43
|
+
@objectid_columns_manager ||= ::ObjectidColumns::ObjectidColumnsManager.new(self)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,298 @@
|
|
1
|
+
require 'objectid_columns/dynamic_methods_module'
|
2
|
+
|
3
|
+
module ObjectidColumns
|
4
|
+
# The ObjectidColumnsManager does all the real work of the ObjectidColumns gem, in many ways -- it takes care of
|
5
|
+
# reading ObjectId values and transforming them to objects, transforming supplied data to the right format when
|
6
|
+
# writing them, handling primary-key definitions and queries.
|
7
|
+
#
|
8
|
+
# This is a separate class, rather than being mixed into the actual ActiveRecord class, so that we can add methods
|
9
|
+
# and define constants here without polluting the namespace of the underlying class.
|
10
|
+
class ObjectidColumnsManager
|
11
|
+
# NOTE: These constants are used in a metaprogrammed fashion in #has_objectid_columns, below. If you rename them,
|
12
|
+
# you must change that, too.
|
13
|
+
BINARY_OBJECTID_LENGTH = 12
|
14
|
+
STRING_OBJECTID_LENGTH = 24
|
15
|
+
|
16
|
+
# Creates a new instance. There should only ever be a single instance for a given ActiveRecord class, accessible
|
17
|
+
# via ObjectidColumns::HasObjectidColumns.objectid_columns_manager.
|
18
|
+
def initialize(active_record_class)
|
19
|
+
raise ArgumentError, "You must supply a Class, not: #{active_record_class.inspect}" unless active_record_class.kind_of?(Class)
|
20
|
+
raise ArgumentError, "You must supply a Class that's a descendant of ActiveRecord::Base, not: #{active_record_class.inspect}" unless superclasses(active_record_class).include?(::ActiveRecord::Base)
|
21
|
+
|
22
|
+
@active_record_class = active_record_class
|
23
|
+
@oid_columns = { }
|
24
|
+
|
25
|
+
# We use a DynamicMethodsModule to add our magic to the target ActiveRecord class, rather than just defining
|
26
|
+
# methods directly on the class, for a number of very good reasons -- see the class comment on
|
27
|
+
# DynamicMethodsModule for more information.
|
28
|
+
@dynamic_methods_module = ObjectidColumns::DynamicMethodsModule.new(active_record_class, :ObjectidColumnsDynamicMethods)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Declares that this class is using an ObjectId as its primary key. Ordinarily, this requires no arguments;
|
32
|
+
# however, if your primary key is not named +id+ and you have not yet told ActiveRecord this (using
|
33
|
+
# <tt>self.primary_key = :foo</tt>), then you must pass the name of the primary-key column.
|
34
|
+
#
|
35
|
+
# Note that, unlike normal database-generated primary keys, this will cause us to auto-generate an ObjectId
|
36
|
+
# primary key value for a new record just before saving it to the database (ActiveRecord's +before_create hook).
|
37
|
+
# ObjectIds are safe to generate client-side, and very difficult to properly generate server-side in a relational
|
38
|
+
# database. However, we will respect (and not overwrite) any primary key already assigned to the record before it's
|
39
|
+
# saved, so if you want to assign your own ObjectId primary keys, you can.
|
40
|
+
def has_objectid_primary_key(primary_key_name = nil)
|
41
|
+
# The Symbol-vs.-String distinction is critical when dealing with old versions of ActiveRecord; for example, if
|
42
|
+
# you say <tt>self.primary_key = :foo</tt> (instead of <tt>self.primary_key = 'foo'</tt>) to older versions of
|
43
|
+
# ActiveRecord, you can end up with some seriously weird errors later (like models trying to save themselves with
|
44
|
+
# _both_ a +:foo+ and a +'foo'+ attribute -- ick!). Boo, AR.
|
45
|
+
primary_key_name = primary_key_name.to_s if primary_key_name
|
46
|
+
pk = active_record_class.primary_key
|
47
|
+
|
48
|
+
# Make sure we know what the primary key is!
|
49
|
+
if (! pk) && (! primary_key_name)
|
50
|
+
raise ArgumentError, "Class #{active_record_class.name} has no primary key set, and you haven't supplied one to .has_objectid_primary_key. Either set one before this call (using self.primary_key = :foo), or supply one to this call (has_objectid_primary_key :foo) and we'll set it for you."
|
51
|
+
end
|
52
|
+
|
53
|
+
pk = pk.to_s if pk
|
54
|
+
|
55
|
+
# Initially, this was a simple +||=+ statement. However, older versions of ActiveRecord will return the string or
|
56
|
+
# symbol +id+ for the primary key if you haven't set an explicit primary key, even if there is no such column on
|
57
|
+
# the underlying table. Again, ick.
|
58
|
+
if (! pk) || (primary_key_name && pk.to_s != primary_key_name.to_s)
|
59
|
+
active_record_class.primary_key = pk = primary_key_name
|
60
|
+
end
|
61
|
+
|
62
|
+
# In case someone is using composite_primary_keys (http://compositekeys.rubyforge.org/).
|
63
|
+
raise "You can't have an ObjectId primary key that's not a String or Symbol: #{pk.inspect}" unless pk.kind_of?(String) || pk.kind_of?(Symbol)
|
64
|
+
|
65
|
+
# Declare our primary-key column as an ObjectId column.
|
66
|
+
has_objectid_column pk
|
67
|
+
|
68
|
+
# If it's not called just "id", we need to explicitly define an "id" method that correctly reads from and writes
|
69
|
+
# to the table.
|
70
|
+
unless pk.to_s == 'id'
|
71
|
+
p = pk
|
72
|
+
dynamic_methods_module.define_method("id") { read_objectid_column(p) }
|
73
|
+
dynamic_methods_module.define_method("id=") { |new_value| write_objectid_column(p, new_value) }
|
74
|
+
end
|
75
|
+
|
76
|
+
# Allow us to autogenerate the primary key, if needed, on save.
|
77
|
+
active_record_class.send(:before_create, :assign_objectid_primary_key)
|
78
|
+
|
79
|
+
# Override a couple of methods that, if you're using an ObjectId column as your primary key, need overriding. ;)
|
80
|
+
[ :find, :find_by_id ].each do |class_method_name|
|
81
|
+
@dynamic_methods_module.define_class_method(class_method_name) do |*args, &block|
|
82
|
+
if args.length == 1 && args[0].kind_of?(String) || ObjectidColumns.is_valid_bson_object?(args[0]) || args[0].kind_of?(Array)
|
83
|
+
args[0] = if args[0].kind_of?(Array)
|
84
|
+
args[0].map { |x| objectid_columns_manager.to_valid_value_for_column(primary_key, x) if x }
|
85
|
+
else
|
86
|
+
objectid_columns_manager.to_valid_value_for_column(primary_key, args[0]) if args[0]
|
87
|
+
end
|
88
|
+
|
89
|
+
super(args[0], &block)
|
90
|
+
else
|
91
|
+
super(*args, &block)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Declares one or more columns as containing ObjectId values. After this call, they can be written using a String
|
98
|
+
# in hex or binary formats, or an ObjectId object; they will return ObjectId objects for values, and can be queried
|
99
|
+
# using any of the above (as long as you use the <tt>where(:foo_oid => ...)</tt> Hash-style syntax).
|
100
|
+
#
|
101
|
+
# If you don't pass in any column names, this will look for columns that end in +_oid+ and assume those are
|
102
|
+
# ObjectId columns.
|
103
|
+
def has_objectid_columns(*columns)
|
104
|
+
return unless active_record_class.table_exists?
|
105
|
+
|
106
|
+
# Autodetect columns ending in +_oid+ if needed
|
107
|
+
columns = autodetect_columns if columns.length == 0
|
108
|
+
|
109
|
+
columns = columns.map { |c| c.to_s.strip.downcase.to_sym }
|
110
|
+
columns.each do |column_name|
|
111
|
+
# Go fetch the column object from the ActiveRecord class, and make sure it's present and of the right type.
|
112
|
+
column_object = active_record_class.columns.detect { |c| c.name.to_s == column_name.to_s }
|
113
|
+
|
114
|
+
unless column_object
|
115
|
+
raise ArgumentError, "#{active_record_class.name} doesn't seem to have a column named #{column_name.inspect} that we could make an ObjectId column; did you misspell it? It has columns: #{active_record_class.columns.map(&:name).inspect}"
|
116
|
+
end
|
117
|
+
|
118
|
+
unless [ :string, :binary ].include?(column_object.type)
|
119
|
+
raise ArgumentError, "#{active_record_class.name} has a column named #{column_name.inspect}, but it is of type #{column_object.type.inspect}; we can only make ObjectId columns out of :string or :binary columns"
|
120
|
+
end
|
121
|
+
|
122
|
+
# Is the column long enough to contain the data we'll need to put in it?
|
123
|
+
required_length = self.class.const_get("#{column_object.type.to_s.upcase}_OBJECTID_LENGTH")
|
124
|
+
# The ||= is in case there's no limit on the column at all -- for example, PostgreSQL +bytea+ columns
|
125
|
+
# behave this way.
|
126
|
+
unless (column_object.limit || required_length + 1) >= required_length
|
127
|
+
raise ArgumentError, "#{active_record_class.name} has a column named #{column_name.inspect} of type #{column_object.type.inspect}, but it is of length #{column_object.limit}, which is too short to contain an ObjectId of this format; it must be of length at least #{required_length}"
|
128
|
+
end
|
129
|
+
|
130
|
+
# Define reader and writer methods that just call through to ObjectidColumns::HasObjectidColumns (which, in
|
131
|
+
# turn, just delegates the call back to this object -- the #read_objectid_column method below; the one on
|
132
|
+
# HasObjectidColumns just passes through the model object itself).
|
133
|
+
cn = column_name
|
134
|
+
dynamic_methods_module.define_method(column_name) do
|
135
|
+
read_objectid_column(cn)
|
136
|
+
end
|
137
|
+
|
138
|
+
dynamic_methods_module.define_method("#{column_name}=") do |x|
|
139
|
+
write_objectid_column(cn, x)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Store away the fact that we've done this.
|
143
|
+
@oid_columns[column_name] = column_object.type
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Called from ObjectidColumns::HasObjectidColumns#read_objectid_column -- given a model and a column name (which
|
148
|
+
# must be an ObjectId column), returns the data in it, as an ObjectId.
|
149
|
+
def read_objectid_column(model, column_name)
|
150
|
+
column_name = column_name.to_s
|
151
|
+
value = model[column_name]
|
152
|
+
return value unless value # in case it's nil
|
153
|
+
|
154
|
+
# If it's not nil, the database should always be giving us back a String...
|
155
|
+
unless value.kind_of?(String)
|
156
|
+
raise "When trying to read the ObjectId column #{column_name.inspect} on #{inspect}, we got the following data from the database; we expected a String: #{value.inspect}"
|
157
|
+
end
|
158
|
+
|
159
|
+
# ugh...ActiveRecord 3.1.x can return this in certain circumstances
|
160
|
+
return nil if value.length == 0
|
161
|
+
|
162
|
+
# In many databases, if you have a column that is, _e.g._, BINARY(16), and you only store twelve bytes in it,
|
163
|
+
# you get back all 16 anyway, with 0x00 bytes at the end. Converting this to an ObjectId will fail, so we make
|
164
|
+
# sure we chop those bytes off. (Note that while String#strip will, in fact, remove these bytes too, it is not
|
165
|
+
# safe: if the ObjectId itself ends in one or more 0x00 bytes, then these will get incorrectly removed.)
|
166
|
+
case objectid_column_type(column_name)
|
167
|
+
when :binary then value = value[0..(BINARY_OBJECTID_LENGTH - 1)]
|
168
|
+
when :string then value = value[0..(STRING_OBJECTID_LENGTH - 1)]
|
169
|
+
else unknown_type(type)
|
170
|
+
end
|
171
|
+
|
172
|
+
# +lib/objectid_columns/extensions.rb+ adds this method to String.
|
173
|
+
value.to_bson_id
|
174
|
+
end
|
175
|
+
|
176
|
+
# Called from ObjectidColumns::HasObjectidColumns#write_objectid_column -- given a model, a column name (which must
|
177
|
+
# be an ObjectId column) and a new value, stores that value in the column.
|
178
|
+
def write_objectid_column(model, column_name, new_value)
|
179
|
+
column_name = column_name.to_s
|
180
|
+
if (! new_value)
|
181
|
+
model[column_name] = new_value
|
182
|
+
elsif new_value.respond_to?(:to_bson_id)
|
183
|
+
model[column_name] = to_valid_value_for_column(column_name, new_value)
|
184
|
+
else
|
185
|
+
raise ArgumentError, "When trying to write the ObjectId column #{column_name.inspect} on #{inspect}, we were passed the following value, which doesn't seem to be a valid BSON ID in any format: #{new_value.inspect}"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
alias_method :has_objectid_column, :has_objectid_columns
|
190
|
+
|
191
|
+
# Given a value for an ObjectId column -- could be a String in either hex or binary formats, or an
|
192
|
+
# ObjectId object -- returns a String of the correct type for the given column (_i.e._, either the binary or hex
|
193
|
+
# String representation of an ObjectId, depending on the type of the underlying column).
|
194
|
+
def to_valid_value_for_column(column_name, value)
|
195
|
+
out = value.to_bson_id
|
196
|
+
unless ObjectidColumns.is_valid_bson_object?(out)
|
197
|
+
raise "We called #to_bson_id on #{value.inspect}, but it returned this, which is not a BSON ID object: #{out.inspect}"
|
198
|
+
end
|
199
|
+
|
200
|
+
case objectid_column_type(column_name)
|
201
|
+
when :binary then out = out.to_binary
|
202
|
+
when :string then out = out.to_s
|
203
|
+
else unknown_type(type)
|
204
|
+
end
|
205
|
+
|
206
|
+
out
|
207
|
+
end
|
208
|
+
|
209
|
+
# Given a key in a Hash supplied to +where+ for the given ActiveRecord class, returns a two-element Array
|
210
|
+
# consisting of the key and the proper value we should actually use to query on that column. If the key does not
|
211
|
+
# represent an ObjectID column, then this will just be exactly the data passed in; however, if it does represent
|
212
|
+
# an ObjectId column, then the value will be translated to whichever String format (binary or hex) that column is
|
213
|
+
# using.
|
214
|
+
#
|
215
|
+
# We use this in ObjectidColumns:;ActiveRecord::Relation#where to make the following work properly:
|
216
|
+
#
|
217
|
+
# MyModel.where(:foo_oid => BSON::ObjectId('52ec126d78161f56d8000001'))
|
218
|
+
#
|
219
|
+
# This method is used to translate this to:
|
220
|
+
#
|
221
|
+
# MyModel.where(:foo_oid => "52ec126d78161f56d8000001")
|
222
|
+
def translate_objectid_query_pair(query_key, query_value)
|
223
|
+
if (type = oid_columns[query_key.to_sym])
|
224
|
+
|
225
|
+
# Handle nil, false
|
226
|
+
if (! query_value)
|
227
|
+
[ query_key, query_value ]
|
228
|
+
|
229
|
+
# +lib/objectid_columns/extensions.rb+ adds String#to_bson_id
|
230
|
+
elsif query_value.respond_to?(:to_bson_id)
|
231
|
+
v = query_value.to_bson_id
|
232
|
+
v = case type
|
233
|
+
when :binary then v.to_binary
|
234
|
+
when :string then v.to_s
|
235
|
+
else unknown_type(type)
|
236
|
+
end
|
237
|
+
[ query_key, v ]
|
238
|
+
|
239
|
+
# Handle arrays of values
|
240
|
+
elsif query_value.kind_of?(Array)
|
241
|
+
array = query_value.map do |v|
|
242
|
+
translate_objectid_query_pair(query_key, v)[1]
|
243
|
+
end
|
244
|
+
[ query_key, array ]
|
245
|
+
|
246
|
+
# Um...what did you pass?
|
247
|
+
else
|
248
|
+
raise ArgumentError, "You're trying to constrain #{active_record_class.name} on column #{query_key.inspect}, which is an ObjectId column, but the value you passed, #{query_value.inspect}, is not a valid format for an ObjectId."
|
249
|
+
end
|
250
|
+
else
|
251
|
+
[ query_key, query_value ]
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
private
|
256
|
+
attr_reader :active_record_class, :dynamic_methods_module, :oid_columns
|
257
|
+
|
258
|
+
# Given the name of a column -- which must be an ObjectId column -- returns its type, either +:binary+ or
|
259
|
+
# +:string+.
|
260
|
+
def objectid_column_type(column_name)
|
261
|
+
out = oid_columns[column_name.to_sym]
|
262
|
+
raise "Something is horribly wrong; #{column_name.inspect} is not an ObjectId column -- we have: #{oid_columns.keys.inspect}" unless out
|
263
|
+
out
|
264
|
+
end
|
265
|
+
|
266
|
+
# Raises an exception -- used for +case+ statements where we switch on the type of the column. Useful so that if,
|
267
|
+
# in the future, we support a new column type, we won't forget to add a case for it in various places.
|
268
|
+
def unknown_type(type)
|
269
|
+
raise "Bug in ObjectidColumns in this method -- type #{type.inspect} does not have a case here."
|
270
|
+
end
|
271
|
+
|
272
|
+
# What's the entire superclass chain of the given class? Used in the constructor to make sure something is
|
273
|
+
# actually a descendant of ActiveRecord::Base.
|
274
|
+
def superclasses(klass)
|
275
|
+
out = [ ]
|
276
|
+
while (sc = klass.superclass)
|
277
|
+
out << sc
|
278
|
+
klass = sc
|
279
|
+
end
|
280
|
+
out
|
281
|
+
end
|
282
|
+
|
283
|
+
# If someone called +has_objectid_columns+ but didn't pass an argument, this method detects which columns we should
|
284
|
+
# automatically turn into ObjectId columns -- which means any columns ending in +_oid+, except for the primary key.
|
285
|
+
def autodetect_columns
|
286
|
+
out = active_record_class.columns.select { |c| c.name =~ /_oid$/i }.map(&:name).map(&:to_s)
|
287
|
+
|
288
|
+
# Make sure we never, ever automatically make the primary-key column an ObjectId column.
|
289
|
+
out -= [ active_record_class.primary_key ].compact.map(&:to_s)
|
290
|
+
|
291
|
+
unless out.length > 0
|
292
|
+
raise ArgumentError, "You didn't pass in the names of any ObjectId columns, and we couldn't find any columns ending in _oid to pick up automatically (primary key is always excluded). Either name some columns explicitly, or remove the has_objectid_columns call."
|
293
|
+
end
|
294
|
+
|
295
|
+
out
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require "objectid_columns/version"
|
2
|
+
require "objectid_columns/active_record/base"
|
3
|
+
require "objectid_columns/active_record/relation"
|
4
|
+
require "active_record"
|
5
|
+
|
6
|
+
# This is the root module for ObjectidColumns. It contains largely just configuration and integration information;
|
7
|
+
# all the real work is done in ObjectidColumns::ObjectidColumnsManager. ObjectidColumns gets its start through the
|
8
|
+
# module ObjectidColumns::ActiveRecord::Base, which gets mixed into ::ActiveRecord::Base (below) and is where methods
|
9
|
+
# like +has_objectid_columns+ are declared.
|
10
|
+
module ObjectidColumns
|
11
|
+
class << self
|
12
|
+
# This is the set of classes we support for representing ObjectIds, as objects, themselves. BSON::ObjectId comes
|
13
|
+
# from the +bson+ gem, and Moped::BSON::ObjectId comes from the +moped+ gem.
|
14
|
+
#
|
15
|
+
# Note that this gem does not declare a dependency on either one of these gems, because we want you to be able to
|
16
|
+
# use either, and there's currently no way of expressing that explicitly. Instead,
|
17
|
+
# .available_objectid_columns_bson_classes, below, takes care of figuring out which ones are availble and loading
|
18
|
+
# them.
|
19
|
+
#
|
20
|
+
# Order is important here: when creating new ObjectId objects (such as when reading from an ObjectId column), we
|
21
|
+
# will prefer the earliest one of these classes that is actually defined. You can change this using
|
22
|
+
# .preferred_bson_class=, below.
|
23
|
+
#
|
24
|
+
#
|
25
|
+
# Any class added here has to obey the following constraints:
|
26
|
+
#
|
27
|
+
# * You can create a new instance from a hex string using .from_string(hex_string)
|
28
|
+
# * Calling #to_s on it returns a hexadecimal String of exactly 24 characters
|
29
|
+
#
|
30
|
+
# Both these objects currently do. If they change, or you want to introduce a new ObjectId representation class,
|
31
|
+
# a small amount of refactoring will be necessary.
|
32
|
+
SUPPORTED_OBJECTID_BSON_CLASS_NAMES = %w{BSON::ObjectId Moped::BSON::ObjectId}
|
33
|
+
|
34
|
+
# When we create a new ObjectId object (such as when reading from a column), what class should it be? If you have
|
35
|
+
# multiple classes loaded and you don't like this one, you can call .preferred_bson_class=, below.
|
36
|
+
def preferred_bson_class
|
37
|
+
@preferred_bson_class ||= available_objectid_columns_bson_classes.first
|
38
|
+
end
|
39
|
+
|
40
|
+
# Sets the preferred BSON class to the given class.
|
41
|
+
def preferred_bson_class=(bson_class)
|
42
|
+
unless SUPPORTED_OBJECTID_BSON_CLASS_NAMES.include?(bson_class.name)
|
43
|
+
raise ArgumentError, "ObjectidColumns does not support BSON class #{bson_class.name}; it supports: #{SUPPORTED_OBJECTID_BSON_CLASS_NAMES.inspect}"
|
44
|
+
end
|
45
|
+
|
46
|
+
@preferred_bson_class = bson_class
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns an array of Class objects -- of length at least 1, but potentially more than 1 -- of the various
|
50
|
+
# ObjectId classes we have available to use. Again, because we don't explicitly depend on the BSON gems
|
51
|
+
# (see above), this needs to take care of trying to load and require the gems in question, and fail gracefully
|
52
|
+
# if they're not present.
|
53
|
+
def available_objectid_columns_bson_classes
|
54
|
+
@available_objectid_columns_bson_classes ||= begin
|
55
|
+
# Try to load both gems, but don't fail if there are errors
|
56
|
+
%w{moped bson}.each do |require_name|
|
57
|
+
begin
|
58
|
+
gem require_name
|
59
|
+
rescue Gem::LoadError => le
|
60
|
+
end
|
61
|
+
|
62
|
+
begin
|
63
|
+
require require_name
|
64
|
+
rescue LoadError => le
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# See which classes we have managed to load
|
69
|
+
defined_classes = SUPPORTED_OBJECTID_BSON_CLASS_NAMES.map do |name|
|
70
|
+
eval("if defined?(#{name}) then #{name} end")
|
71
|
+
end.compact
|
72
|
+
|
73
|
+
# Raise an error if we haven't loaded either
|
74
|
+
if defined_classes.length == 0
|
75
|
+
raise %{ObjectidColumns requires a library that implements an ObjectId class to be loaded; we support
|
76
|
+
the following ObjectId classes: #{SUPPORTED_OBJECTID_BSON_CLASS_NAMES.join(", ")}.
|
77
|
+
(These are from the 'bson' or 'moped' gems.) You seem to have neither one installed.
|
78
|
+
|
79
|
+
Please add one of these gems to your project and try again. Usually, this just means
|
80
|
+
adding this to your Gemfile:
|
81
|
+
|
82
|
+
gem 'bson'
|
83
|
+
|
84
|
+
(ObjectidColumns does not explicitly depend on either of these, because we want you
|
85
|
+
to be able to choose whichever one you prefer.)}
|
86
|
+
end
|
87
|
+
|
88
|
+
defined_classes
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Is the given object a valid BSON ObjectId? This doesn't count Strings in any format -- only objects.
|
93
|
+
def is_valid_bson_object?(x)
|
94
|
+
available_objectid_columns_bson_classes.detect { |k| x.kind_of?(k) }
|
95
|
+
end
|
96
|
+
|
97
|
+
# Creates a new BSON ObjectId from the given String, which must be in the hexadecimal format.
|
98
|
+
def construct_objectid(hex_string)
|
99
|
+
preferred_bson_class.send(:from_string, hex_string)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Creates a new BSON ObjectId, from scratch; this must return an ObjectId with a value, suitable for assigning
|
103
|
+
# to a newly-created row. We use this only if you've declared that your primary key column is an ObjectId, and
|
104
|
+
# then only if you're about to save a new row and it has no ID yet.
|
105
|
+
def new_objectid
|
106
|
+
preferred_bson_class.new
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Include the modules that add the initial methods to ActiveRecord::Base, like +has_objectid_columns+.
|
112
|
+
::ActiveRecord::Base.class_eval do
|
113
|
+
include ::ObjectidColumns::ActiveRecord::Base
|
114
|
+
end
|
115
|
+
|
116
|
+
# This adds our patch to +#where+, so that queries will work properly (assuming you use Hash-style syntax).
|
117
|
+
::ActiveRecord::Relation.class_eval do
|
118
|
+
include ::ObjectidColumns::ActiveRecord::Relation
|
119
|
+
end
|
120
|
+
|
121
|
+
require "objectid_columns/extensions"
|