object_id_gem 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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