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