object_id_gem 1.0.6
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 +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
|