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
@@ -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"
|