object_id_gem 1.0.6
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 +44 -0
- data/CHANGES.md +23 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +225 -0
- data/Rakefile +6 -0
- data/lib/objectid_columns.rb +127 -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/arel/visitors/to_sql.rb +88 -0
- data/lib/objectid_columns/dynamic_methods_module.rb +127 -0
- data/lib/objectid_columns/extensions.rb +41 -0
- data/lib/objectid_columns/has_objectid_columns.rb +47 -0
- data/lib/objectid_columns/objectid_columns_manager.rb +451 -0
- data/lib/objectid_columns/version.rb +4 -0
- data/object_id_gem.gemspec +71 -0
- data/spec/objectid_columns/helpers/database_helper.rb +178 -0
- data/spec/objectid_columns/helpers/system_helpers.rb +92 -0
- data/spec/objectid_columns/system/basic_system_spec.rb +600 -0
- data/spec/objectid_columns/system/extensions_spec.rb +69 -0
- metadata +194 -0
@@ -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,88 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
module ObjectidColumns
|
4
|
+
module Arel
|
5
|
+
module Visitors
|
6
|
+
# This module gets mixed into Arel::Visitors::ToSql, which is the class that the Arel gem (which is really the
|
7
|
+
# backbone of ActiveRecord's query language) uses to generate SQL. This teaches Arel what to do when it bumps
|
8
|
+
# into an object of a BSON ID class -- _i.e._, how to convert it to a SQL literal.
|
9
|
+
#
|
10
|
+
# How this works depends on which version of ActiveRecord -- and therefore AREL -- you're using:
|
11
|
+
#
|
12
|
+
# * In Arel 4.x, the #visit... methods get called with two arguments. The first is the actual BSON ID that needs
|
13
|
+
# to be converted; the second provides context. From the second parameter, we can get the table name and
|
14
|
+
# column name. We use this to get a hold of the ObjectidColumnsManager via its class method .for_table, and,
|
15
|
+
# from there, a converted, valid value for the column in question (whether hex or binary).
|
16
|
+
# * In Arel 2.x (AR 3.0.x) and 3.x, we have to monkeypatch the #visit_Arel_Attributes_Attribute method -- it
|
17
|
+
# already picks up and stashes away the .last_column, but we need to add the .last_relation, too.
|
18
|
+
#
|
19
|
+
module ToSql
|
20
|
+
extend ActiveSupport::Concern
|
21
|
+
|
22
|
+
require 'arel'
|
23
|
+
if ::Arel::VERSION =~ /^[23]\./
|
24
|
+
def visit_Arel_Attributes_Attribute_with_objectid_columns(o, *args)
|
25
|
+
out = visit_Arel_Attributes_Attribute_without_objectid_columns(o, *args)
|
26
|
+
self.last_relation = o.relation
|
27
|
+
out
|
28
|
+
end
|
29
|
+
|
30
|
+
included do
|
31
|
+
alias_method_chain :visit_Arel_Attributes_Attribute, :objectid_columns
|
32
|
+
|
33
|
+
alias :visit_Arel_Attributes_Integer :visit_Arel_Attributes_Attribute_with_objectid_columns
|
34
|
+
alias :visit_Arel_Attributes_Float :visit_Arel_Attributes_Attribute_with_objectid_columns
|
35
|
+
alias :visit_Arel_Attributes_Decimal :visit_Arel_Attributes_Attribute_with_objectid_columns
|
36
|
+
alias :visit_Arel_Attributes_String :visit_Arel_Attributes_Attribute_with_objectid_columns
|
37
|
+
alias :visit_Arel_Attributes_Time :visit_Arel_Attributes_Attribute_with_objectid_columns
|
38
|
+
alias :visit_Arel_Attributes_Boolean :visit_Arel_Attributes_Attribute_with_objectid_columns
|
39
|
+
|
40
|
+
attr_accessor :last_relation
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def visit_BSON_ObjectId(o, a = nil)
|
45
|
+
column = if a then column_for(a) else last_column end
|
46
|
+
relation = if a then a.relation else last_relation end
|
47
|
+
|
48
|
+
return quote(o.to_s) unless column && relation
|
49
|
+
|
50
|
+
quote(bson_objectid_value_from_parameter(o, column, relation), column)
|
51
|
+
end
|
52
|
+
|
53
|
+
alias_method :visit_Moped_BSON_ObjectId, :visit_BSON_ObjectId
|
54
|
+
|
55
|
+
private
|
56
|
+
def bson_objectid_value_from_parameter(o, column, relation)
|
57
|
+
column_name = column.name
|
58
|
+
|
59
|
+
manager = ObjectidColumns::ObjectidColumnsManager.for_table(relation.name)
|
60
|
+
unless manager
|
61
|
+
raise %{ObjectidColumns: You're trying to evaluate a SQL statement (in Arel, probably via ActiveRecord)
|
62
|
+
that contains a BSON ObjectId value -- you're trying to use the value '#{o}'
|
63
|
+
(of class #{o.class.name}) with column #{column_name.inspect} of table
|
64
|
+
#{relation.name.inspect}. However, we can't find any record of any ObjectId
|
65
|
+
columns being declared for that table anywhere.
|
66
|
+
|
67
|
+
As a result, we don't know whether this column should be treated as a binary or
|
68
|
+
a hexadecimal ObjectId, and hence don't know how to transform this value properly.}
|
69
|
+
end
|
70
|
+
|
71
|
+
unless manager.is_objectid_column?(column_name)
|
72
|
+
raise %{ObjectidColumns: You're trying to evaluate a SQL statement (in Arel, probably via ActiveRecord)
|
73
|
+
that contains a BSON ObjectId value -- you're trying to use the value '#{o}'
|
74
|
+
(of class #{o.class.name}) with column #{column_name.inspect} of table
|
75
|
+
#{relation.name.inspect}.
|
76
|
+
|
77
|
+
While we can find a record of some ObjectId columns being declared for
|
78
|
+
that table, they don't appear to include #{column_name.inspect}. As such,
|
79
|
+
we don't know whether this column should be treated as a binary or a hexadecimal
|
80
|
+
ObjectId, and hence don't know how to transform this value properly.}
|
81
|
+
end
|
82
|
+
|
83
|
+
manager.to_valid_value_for_column(column_name, o)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,127 @@
|
|
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
|
+
|
48
|
+
# Sigh. From the docs for Method#arity:
|
49
|
+
#
|
50
|
+
# "For Ruby methods that take a variable number of arguments, returns -n-1, where n is the number of required
|
51
|
+
# arguments. For methods written in C, returns -1 if the call takes a variable number of arguments."
|
52
|
+
#
|
53
|
+
# It turns out that .const_defined? is written in C, which means it returns -1 if it takes a variable number of
|
54
|
+
# arguments. So we can't check for arity.abs >= 2 here, but rather must look for <= -1...
|
55
|
+
if @target_class.method(:const_defined?).arity <= -1
|
56
|
+
if @target_class.const_defined?(@name, false)
|
57
|
+
existing = @target_class.const_get(@name, false)
|
58
|
+
|
59
|
+
if existing && existing != self
|
60
|
+
raise NameError, %{You tried to define a #{self.class.name} named #{name.inspect} on class #{target_class.name},
|
61
|
+
but that class already has a constant named #{name.inspect}: #{existing.inspect}}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
else
|
65
|
+
# So...in Ruby 1.8.7, .const_defined? and .const_get don't accept the second parameter, which tells you whether
|
66
|
+
# to search superclass constants as well. But, amusingly, we're not only not stuck, this is fine: in Ruby
|
67
|
+
# 1.8.7, constant lookup doesn't search superclasses, either -- so we're OK.
|
68
|
+
if @target_class.const_defined?(@name)
|
69
|
+
existing = @target_class.const_get(@name)
|
70
|
+
|
71
|
+
if existing && existing != self
|
72
|
+
raise NameError, %{You tried to define a #{self.class.name} named #{name.inspect} on class #{target_class.name},
|
73
|
+
but that class already has a constant named #{name.inspect}: #{existing.inspect}}
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
@class_methods_module = Module.new
|
80
|
+
(class << @class_methods_module; self; end).send(:public, :private)
|
81
|
+
@target_class.const_set("#{@name}ClassMethods", @class_methods_module)
|
82
|
+
@target_class.send(:extend, @class_methods_module)
|
83
|
+
|
84
|
+
@target_class.const_set(@name, self)
|
85
|
+
@target_class.send(:include, self)
|
86
|
+
|
87
|
+
@methods_defined = { }
|
88
|
+
@class_methods_defined = { }
|
89
|
+
|
90
|
+
super(&block)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Removes all methods that have been defined on this module using #define_method, below. (If you use some other
|
94
|
+
# mechanism to define a method on this DynamicMethodsModule, then it will not be removed when this method is
|
95
|
+
# called.)
|
96
|
+
def remove_all_methods!
|
97
|
+
instance_methods.each do |method_name|
|
98
|
+
# Important -- we use Class#remove_method, not Class#undef_method, which does something that's different in
|
99
|
+
# some important ways.
|
100
|
+
remove_method(method_name) if @methods_defined[method_name.to_sym]
|
101
|
+
end
|
102
|
+
|
103
|
+
@class_methods_module.instance_methods.each do |method_name|
|
104
|
+
@class_methods_module.send(:remove_method, method_name) if @class_methods_defined[method_name]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Defines a method. Works identically to Module#define_method, except that it's +public+ and #remove_all_methods!
|
109
|
+
# will remove the method.
|
110
|
+
def define_method(name, &block)
|
111
|
+
name = name.to_sym
|
112
|
+
super(name, &block)
|
113
|
+
@methods_defined[name] = true
|
114
|
+
end
|
115
|
+
|
116
|
+
# Defines a class method.
|
117
|
+
def define_class_method(name, &block)
|
118
|
+
@class_methods_module.send(:define_method, name, &block)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Makes it so you can say, for example:
|
122
|
+
#
|
123
|
+
# my_dynamic_methods_module.define_method(:foo) { ... }
|
124
|
+
# my_dynamic_methods_module.private(:foo)
|
125
|
+
public :private # teehee
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,41 @@
|
|
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
|
+
encoding_string = respond_to?(:encoding) ? ", in encoding #{encoding.inspect}" : ""
|
38
|
+
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. It is #{length} characters long#{encoding_string}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
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.class.objectid_columns_manager.assign_objectid_primary_key(self)
|
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,451 @@
|
|
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
|
+
|
30
|
+
self.class.register_for_table(active_record_class.table_name, self)
|
31
|
+
end
|
32
|
+
|
33
|
+
class << self
|
34
|
+
# ObjectidColumns::Arel::Visitors::ToSql needs to be able to figure out whether an ObjectId column is of binary
|
35
|
+
# or text format, in order to properly transform/quote the value it has. However, by the time the code gets there,
|
36
|
+
# we no longer have access to the ActiveRecord model at all. So, instead, we need an entry point to be able to
|
37
|
+
# find the ObjectidColumnsManager for a table by name. That's .for_table, below; this is the method called at
|
38
|
+
# the end of the constructor of every ObjectidColumnsManager, registering the instance by table name.
|
39
|
+
def register_for_table(table_name, instance)
|
40
|
+
@_registered_instances ||= { }
|
41
|
+
@_registered_instances[table_name] = instance
|
42
|
+
end
|
43
|
+
|
44
|
+
# See above. Given a table name, this returns the ObjectidColumnsManager for it, or +nil+ if none has been
|
45
|
+
# defined for that table.
|
46
|
+
def for_table(table_name)
|
47
|
+
@_registered_instances[table_name]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# This method basically says: does our +active_record_class+ have a primary key defined, for real? There are two
|
52
|
+
# reasons this is anything more than (<tt>!! active_record_class.primary_key</tt>):
|
53
|
+
#
|
54
|
+
# * In earlier versions of ActiveRecord (like 3.0.x), this will return +id+ even if you haven't set it and there is
|
55
|
+
# no column named +id+.
|
56
|
+
# * The +composite_primary_keys+ gem can make this an array instead.
|
57
|
+
def activerecord_class_has_no_real_primary_key?
|
58
|
+
(! active_record_class.primary_key) ||
|
59
|
+
(active_record_class.primary_key == [ ]) ||
|
60
|
+
( ([ [ 'id' ], [ :id ] ].include?(Array(active_record_class.primary_key))) &&
|
61
|
+
(! active_record_class.columns_hash.has_key?('id')) &&
|
62
|
+
(! active_record_class.columns_hash.has_key?(:id)))
|
63
|
+
end
|
64
|
+
|
65
|
+
# If you haven't specified a primary key on your model (using <tt>self.primary_key=</tt>), and you call
|
66
|
+
# +has_objectid_primary_key+, we want to tell the ActiveRecord model that that's the new primary key. This takes
|
67
|
+
# care of that, and handles the fact that this may be a composite primary key, too.
|
68
|
+
def set_primary_key_from!(primary_keys)
|
69
|
+
if primary_keys.length > 1
|
70
|
+
active_record_class.primary_key = primary_keys.map(&:to_s)
|
71
|
+
elsif primary_keys.length == 1
|
72
|
+
active_record_class.primary_key = primary_keys[0].to_s
|
73
|
+
else
|
74
|
+
# nothing here; we handle this elsewhere
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Assigns a new ObjectId primary key to a brand-new model that's about to be created, if needed. This handles
|
79
|
+
# composite primary keys correctly.
|
80
|
+
def assign_objectid_primary_key(model)
|
81
|
+
Array(model.class.primary_key).each do |pk_column|
|
82
|
+
if is_objectid_column?(pk_column) && model[pk_column].blank?
|
83
|
+
model.send("#{pk_column}=", ObjectidColumns.new_objectid)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Given a model, returns the correct value for #id. This takes into account composite primary keys where some
|
89
|
+
# columns may be ObjectId columns and some may not.
|
90
|
+
def read_objectid_primary_key(model)
|
91
|
+
pks = Array(model.class.primary_key)
|
92
|
+
out = [ ]
|
93
|
+
pks.each do |pk_column|
|
94
|
+
out << if is_objectid_column?(pk_column)
|
95
|
+
read_objectid_column(model, pk_column)
|
96
|
+
else
|
97
|
+
model[pk_column]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
out = out[0] if out.length == 1
|
101
|
+
out
|
102
|
+
end
|
103
|
+
|
104
|
+
# Given a model, stores a new value for #id. This takes into account composite primary keys where some
|
105
|
+
# columns may be ObjectId columns and some may not.
|
106
|
+
def write_objectid_primary_key(model, new_value)
|
107
|
+
pks = Array(model.class.primary_key)
|
108
|
+
if pks.length == 1
|
109
|
+
write_objectid_column(model, pks[0], new_value)
|
110
|
+
else
|
111
|
+
pks.each_with_index do |pk_column, index|
|
112
|
+
value = new_value[index]
|
113
|
+
if is_objectid_column?(pk_column)
|
114
|
+
write_objectid_column(model, pk_column, value)
|
115
|
+
else
|
116
|
+
model[pk_column] = value
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Implements .find or .find_by_id for classes that have a primary key that has at least one ObjectId column in it;
|
123
|
+
# this takes care of handling both normal primary keys and composite primary keys.
|
124
|
+
def find_or_find_by_id(*args)
|
125
|
+
primary_key = active_record_class.primary_key
|
126
|
+
pk_length = primary_key.kind_of?(Array) ? primary_key.length : 1
|
127
|
+
|
128
|
+
# If we just have a single primary key, we flatten any input, just because that's exactly what base
|
129
|
+
# ActiveRecord does...
|
130
|
+
if pk_length == 1
|
131
|
+
args = args.flatten
|
132
|
+
args = args.map { |x| to_valid_value_for_column(primary_key, x) if x }
|
133
|
+
yield(*args)
|
134
|
+
else
|
135
|
+
# composite_primary_keys, however, requires that you pass each key as a single, separate argument to .find or
|
136
|
+
# .find_by_id; we transform them here.
|
137
|
+
keys = args.map do |key|
|
138
|
+
new_key = [ ]
|
139
|
+
key.each_with_index do |key_component, index|
|
140
|
+
column = primary_key[index]
|
141
|
+
new_key << if is_objectid_column?(column)
|
142
|
+
to_valid_value_for_column(column, key_component) if key_component
|
143
|
+
else
|
144
|
+
key_component
|
145
|
+
end
|
146
|
+
end
|
147
|
+
new_key
|
148
|
+
end
|
149
|
+
yield(*keys)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Declares that this class is using an ObjectId as its primary key. Ordinarily, this requires no arguments;
|
154
|
+
# however, if your primary key is not named +id+ and you have not yet told ActiveRecord this (using
|
155
|
+
# <tt>self.primary_key = :foo</tt>), then you must pass the name of the primary-key column.
|
156
|
+
#
|
157
|
+
# Note that, unlike normal database-generated primary keys, this will cause us to auto-generate an ObjectId
|
158
|
+
# primary key value for a new record just before saving it to the database (ActiveRecord's +before_create hook).
|
159
|
+
# ObjectIds are safe to generate client-side, and very difficult to properly generate server-side in a relational
|
160
|
+
# database. However, we will respect (and not overwrite) any primary key already assigned to the record before it's
|
161
|
+
# saved, so if you want to assign your own ObjectId primary keys, you can.
|
162
|
+
#
|
163
|
+
# This method handles composite primary keys, as provided by the +composite_primary_keys+ gem, correctly.
|
164
|
+
def has_objectid_primary_key(*primary_keys_that_are_objectid_columns)
|
165
|
+
return unless active_record_class.table_exists?
|
166
|
+
|
167
|
+
# First, normalize our set of primary keys that are ObjectId columns...
|
168
|
+
primary_keys_that_are_objectid_columns = primary_keys_that_are_objectid_columns.compact.map(&:to_s).uniq
|
169
|
+
|
170
|
+
# Now, see what all the primary keys are. If the user hasn't specified any primary keys on the class at all yet,
|
171
|
+
# but has told us what they are, then we need to tell ActiveRecord what they are.
|
172
|
+
all_primary_keys = if activerecord_class_has_no_real_primary_key?
|
173
|
+
set_primary_key_from!(primary_keys_that_are_objectid_columns)
|
174
|
+
primary_keys_that_are_objectid_columns
|
175
|
+
else
|
176
|
+
Array(active_record_class.primary_key)
|
177
|
+
end
|
178
|
+
# Normalize the set of all primary keys.
|
179
|
+
all_primary_keys = all_primary_keys.compact.map(&:to_s).uniq
|
180
|
+
|
181
|
+
# Let's make sure we have a primary key...
|
182
|
+
raise ArgumentError, "Class #{active_record_class.name} has no primary key set, and you haven't supplied one to #has_objectid_primary_key" if all_primary_keys.empty?
|
183
|
+
|
184
|
+
# If you didn't specify any ObjectId columns explicitly, use what we know about the class to figure out which
|
185
|
+
# ones you mean.
|
186
|
+
if primary_keys_that_are_objectid_columns.empty?
|
187
|
+
if all_primary_keys.length == 1
|
188
|
+
primary_keys_that_are_objectid_columns = all_primary_keys
|
189
|
+
else
|
190
|
+
primary_keys_that_are_objectid_columns = autodetect_columns_from(all_primary_keys, true)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# Make sure we have at least one ObjectId primary key, if we're in this method.
|
195
|
+
raise "Class #{active_record_class.name} has no columns in its primary key that qualify as object IDs automatically; you must specify their names explicitly." if primary_keys_that_are_objectid_columns.empty?
|
196
|
+
|
197
|
+
# Make sure all the columns the user named actually exist as columns on the model.
|
198
|
+
missing = primary_keys_that_are_objectid_columns.select { |c| ! active_record_class.columns_hash.has_key?(c) }
|
199
|
+
raise "The following primary-key column(s) do not appear to actually exist on #{active_record_class.name}: #{missing.inspect}; we have these columns: #{active_record_class.columns_hash.keys.inspect}" unless missing.empty?
|
200
|
+
|
201
|
+
# Declare our primary-key column as an ObjectId column.
|
202
|
+
has_objectid_column *primary_keys_that_are_objectid_columns
|
203
|
+
|
204
|
+
# Override #id and #id= to do the right thing...
|
205
|
+
dynamic_methods_module.define_method("id") do
|
206
|
+
self.class.objectid_columns_manager.read_objectid_primary_key(self)
|
207
|
+
end
|
208
|
+
dynamic_methods_module.define_method("id=") do |new_value|
|
209
|
+
self.class.objectid_columns_manager.write_objectid_primary_key(self, new_value)
|
210
|
+
end
|
211
|
+
|
212
|
+
# Allow us to autogenerate the primary key, if needed, on save.
|
213
|
+
active_record_class.send(:before_create, :assign_objectid_primary_key)
|
214
|
+
|
215
|
+
# Override a couple of methods that, if you're using an ObjectId column as your primary key, need overriding. ;)
|
216
|
+
[ :find, :find_by_id ].each do |class_method_name|
|
217
|
+
@dynamic_methods_module.define_class_method(class_method_name) do |*args, &block|
|
218
|
+
objectid_columns_manager.find_or_find_by_id(*args) { |*new_args| super(*new_args, &block) }
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# Declares one or more columns as containing ObjectId values. After this call, they can be written using a String
|
224
|
+
# in hex or binary formats, or an ObjectId object; they will return ObjectId objects for values, and can be queried
|
225
|
+
# using any of the above (as long as you use the <tt>where(:foo_oid => ...)</tt> Hash-style syntax).
|
226
|
+
#
|
227
|
+
# If you don't pass in any column names, this will look for columns that end in +_oid+ and assume those are
|
228
|
+
# ObjectId columns.
|
229
|
+
def has_objectid_columns(*columns)
|
230
|
+
return unless active_record_class.table_exists?
|
231
|
+
|
232
|
+
# Autodetect columns ending in +_oid+ if needed
|
233
|
+
columns = autodetect_columns_from(active_record_class.columns_hash.keys) if columns.length == 0
|
234
|
+
|
235
|
+
columns = columns.map { |c| c.to_s.strip.downcase.to_sym }
|
236
|
+
columns.each do |column_name|
|
237
|
+
# Go fetch the column object from the ActiveRecord class, and make sure it's present and of the right type.
|
238
|
+
column_object = active_record_class.columns.detect { |c| c.name.to_s == column_name.to_s }
|
239
|
+
|
240
|
+
unless column_object
|
241
|
+
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}"
|
242
|
+
end
|
243
|
+
|
244
|
+
unless [ :string, :binary ].include?(column_object.type)
|
245
|
+
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"
|
246
|
+
end
|
247
|
+
|
248
|
+
# Is the column long enough to contain the data we'll need to put in it?
|
249
|
+
required_length = self.class.const_get("#{column_object.type.to_s.upcase}_OBJECTID_LENGTH")
|
250
|
+
# The ||= is in case there's no limit on the column at all -- for example, PostgreSQL +bytea+ columns
|
251
|
+
# behave this way.
|
252
|
+
unless (column_object.limit || required_length + 1) >= required_length
|
253
|
+
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}"
|
254
|
+
end
|
255
|
+
|
256
|
+
# Define reader and writer methods that just call through to ObjectidColumns::HasObjectidColumns (which, in
|
257
|
+
# turn, just delegates the call back to this object -- the #read_objectid_column method below; the one on
|
258
|
+
# HasObjectidColumns just passes through the model object itself).
|
259
|
+
cn = column_name
|
260
|
+
dynamic_methods_module.define_method(column_name) do
|
261
|
+
read_objectid_column(cn)
|
262
|
+
end
|
263
|
+
|
264
|
+
dynamic_methods_module.define_method("#{column_name}=") do |x|
|
265
|
+
write_objectid_column(cn, x)
|
266
|
+
end
|
267
|
+
|
268
|
+
# Store away the fact that we've done this.
|
269
|
+
@oid_columns[column_name] = column_object.type
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
# Called from ObjectidColumns::HasObjectidColumns#read_objectid_column -- given a model and a column name (which
|
274
|
+
# must be an ObjectId column), returns the data in it, as an ObjectId.
|
275
|
+
def read_objectid_column(model, column_name)
|
276
|
+
column_name = column_name.to_s
|
277
|
+
value = model[column_name]
|
278
|
+
return value unless value # in case it's nil
|
279
|
+
return value if ObjectidColumns.is_valid_bson_object?(value) # we can get this when reading the 'id' pseudocolumn
|
280
|
+
|
281
|
+
# If it's not nil, the database should always be giving us back a String...
|
282
|
+
unless value.kind_of?(String)
|
283
|
+
raise "When trying to read the ObjectId column #{column_name.inspect} on #{active_record_class.name} ID=#{model.id.inspect}, we got the following data from the database; we expected a String: #{value.inspect}"
|
284
|
+
end
|
285
|
+
|
286
|
+
# ugh...ActiveRecord 3.1.x can return this in certain circumstances
|
287
|
+
return nil if value.length == 0
|
288
|
+
|
289
|
+
# In many databases, if you have a column that is, _e.g._, BINARY(16), and you only store twelve bytes in it,
|
290
|
+
# you get back all 16 anyway, with 0x00 bytes at the end. Converting this to an ObjectId will fail, so we make
|
291
|
+
# sure we chop those bytes off. (Note that while String#strip will, in fact, remove these bytes too, it is not
|
292
|
+
# safe: if the ObjectId itself ends in one or more 0x00 bytes, then these will get incorrectly removed.)
|
293
|
+
case type = objectid_column_type(column_name)
|
294
|
+
when :binary then value = value[0..(BINARY_OBJECTID_LENGTH - 1)]
|
295
|
+
when :string then value = value[0..(STRING_OBJECTID_LENGTH - 1)]
|
296
|
+
else unknown_type(type)
|
297
|
+
end
|
298
|
+
|
299
|
+
# +lib/objectid_columns/extensions.rb+ adds this method to String.
|
300
|
+
value.to_bson_id
|
301
|
+
end
|
302
|
+
|
303
|
+
# Called from ObjectidColumns::HasObjectidColumns#write_objectid_column -- given a model, a column name (which must
|
304
|
+
# be an ObjectId column) and a new value, stores that value in the column.
|
305
|
+
def write_objectid_column(model, column_name, new_value)
|
306
|
+
column_name = column_name.to_s
|
307
|
+
if (! new_value)
|
308
|
+
model[column_name] = new_value
|
309
|
+
elsif new_value.respond_to?(:to_bson_id)
|
310
|
+
model[column_name] = to_valid_value_for_column(column_name, new_value)
|
311
|
+
else
|
312
|
+
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}"
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
alias_method :has_objectid_column, :has_objectid_columns
|
317
|
+
|
318
|
+
# Given a value for an ObjectId column -- could be a String in either hex or binary formats, or an
|
319
|
+
# ObjectId object -- returns a String of the correct type for the given column (_i.e._, either the binary or hex
|
320
|
+
# String representation of an ObjectId, depending on the type of the underlying column).
|
321
|
+
def to_valid_value_for_column(column_name, value)
|
322
|
+
out = value.to_bson_id
|
323
|
+
unless ObjectidColumns.is_valid_bson_object?(out)
|
324
|
+
raise "We called #to_bson_id on #{value.inspect}, but it returned this, which is not a BSON ID object: #{out.inspect}"
|
325
|
+
end
|
326
|
+
|
327
|
+
case objectid_column_type(column_name)
|
328
|
+
when :binary then out = out.to_binary
|
329
|
+
when :string then out = out.to_s
|
330
|
+
else unknown_type(type)
|
331
|
+
end
|
332
|
+
|
333
|
+
out
|
334
|
+
end
|
335
|
+
|
336
|
+
# Given a key in a Hash supplied to +where+ for the given ActiveRecord class, returns a two-element Array
|
337
|
+
# consisting of the key and the proper value we should actually use to query on that column. If the key does not
|
338
|
+
# represent an ObjectID column, then this will just be exactly the data passed in; however, if it does represent
|
339
|
+
# an ObjectId column, then the value will be translated to whichever String format (binary or hex) that column is
|
340
|
+
# using.
|
341
|
+
#
|
342
|
+
# We use this in ObjectidColumns:;ActiveRecord::Relation#where to make the following work properly:
|
343
|
+
#
|
344
|
+
# MyModel.where(:foo_oid => BSON::ObjectId('52ec126d78161f56d8000001'))
|
345
|
+
#
|
346
|
+
# This method is used to translate this to:
|
347
|
+
#
|
348
|
+
# MyModel.where(:foo_oid => "52ec126d78161f56d8000001")
|
349
|
+
def translate_objectid_query_pair(query_key, query_value)
|
350
|
+
if (type = net_oid_columns[query_key.to_sym])
|
351
|
+
|
352
|
+
# Handle nil, false
|
353
|
+
if (! query_value)
|
354
|
+
[ query_key, query_value ]
|
355
|
+
|
356
|
+
# +lib/objectid_columns/extensions.rb+ adds String#to_bson_id
|
357
|
+
elsif query_value.respond_to?(:to_bson_id)
|
358
|
+
v = query_value.to_bson_id
|
359
|
+
v = case type
|
360
|
+
when :binary then v.to_binary
|
361
|
+
when :string then v.to_s
|
362
|
+
else unknown_type(type)
|
363
|
+
end
|
364
|
+
[ query_key, v ]
|
365
|
+
|
366
|
+
# Handle arrays of values
|
367
|
+
elsif query_value.kind_of?(Array)
|
368
|
+
array = query_value.map do |v|
|
369
|
+
translate_objectid_query_pair(query_key, v)[1]
|
370
|
+
end
|
371
|
+
[ query_key, array ]
|
372
|
+
|
373
|
+
# Um...what did you pass?
|
374
|
+
else
|
375
|
+
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."
|
376
|
+
end
|
377
|
+
else
|
378
|
+
[ query_key, query_value ]
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
# Given the name of a column, tell whether or not it is an ObjectId column.
|
383
|
+
def is_objectid_column?(column_name)
|
384
|
+
net_oid_columns.has_key?(column_name.to_sym)
|
385
|
+
end
|
386
|
+
|
387
|
+
# Returns the same thing as +oid_columns+, except merges in the ActiveRecord class's superclass's columns, if
|
388
|
+
# any.
|
389
|
+
def net_oid_columns
|
390
|
+
out = { }
|
391
|
+
if (socm = superclass_objectid_columns_manager)
|
392
|
+
out = socm.net_oid_columns
|
393
|
+
end
|
394
|
+
out.merge(oid_columns)
|
395
|
+
end
|
396
|
+
|
397
|
+
private
|
398
|
+
attr_reader :active_record_class, :dynamic_methods_module, :oid_columns
|
399
|
+
|
400
|
+
# Given the name of a column -- which must be an ObjectId column -- returns its type, either +:binary+ or
|
401
|
+
# +:string+.
|
402
|
+
def objectid_column_type(column_name)
|
403
|
+
out = net_oid_columns[column_name.to_sym]
|
404
|
+
raise "Something is horribly wrong; #{column_name.inspect} is not an ObjectId column -- we have: #{net_oid_columns.keys.inspect}" unless out
|
405
|
+
out
|
406
|
+
end
|
407
|
+
|
408
|
+
# If our +@active_record_class+ has a superclass that in turn is using +objectid_columns+, returns the
|
409
|
+
# ObjectidColumnsManager for that class, if any.
|
410
|
+
def superclass_objectid_columns_manager
|
411
|
+
ar_superclass = @active_record_class.superclass
|
412
|
+
ar_superclass.objectid_columns_manager if ar_superclass.respond_to?(:objectid_columns_manager)
|
413
|
+
end
|
414
|
+
|
415
|
+
# Raises an exception -- used for +case+ statements where we switch on the type of the column. Useful so that if,
|
416
|
+
# in the future, we support a new column type, we won't forget to add a case for it in various places.
|
417
|
+
def unknown_type(type)
|
418
|
+
raise "Bug in ObjectidColumns in this method -- type #{type.inspect} does not have a case here."
|
419
|
+
end
|
420
|
+
|
421
|
+
# What's the entire superclass chain of the given class? Used in the constructor to make sure something is
|
422
|
+
# actually a descendant of ActiveRecord::Base.
|
423
|
+
def superclasses(klass)
|
424
|
+
out = [ ]
|
425
|
+
while (sc = klass.superclass)
|
426
|
+
out << sc
|
427
|
+
klass = sc
|
428
|
+
end
|
429
|
+
out
|
430
|
+
end
|
431
|
+
|
432
|
+
# If someone called +has_objectid_columns+ but didn't pass an argument, this method detects which columns we should
|
433
|
+
# automatically turn into ObjectId columns -- which means any columns ending in +_oid+, except for the primary key.
|
434
|
+
def autodetect_columns_from(column_names, allow_primary_key = false)
|
435
|
+
column_names = column_names.map(&:to_s)
|
436
|
+
out = column_names.select do |column_name|
|
437
|
+
column = active_record_class.columns_hash[column_name]
|
438
|
+
column && column.name =~ /_oid$/i
|
439
|
+
end
|
440
|
+
|
441
|
+
# Make sure we never, ever automatically make the primary-key column an ObjectId column.
|
442
|
+
out -= Array(active_record_class.primary_key).compact.map(&:to_s) unless allow_primary_key
|
443
|
+
|
444
|
+
unless out.length > 0
|
445
|
+
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. We found columns named: #{column_names.inspect}"
|
446
|
+
end
|
447
|
+
|
448
|
+
out
|
449
|
+
end
|
450
|
+
end
|
451
|
+
end
|