objectid_columns 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA512:
3
+ metadata.gz: 8752dd26c637b7ba079438edd6abedce5c7df3fe44965c176f3aa3b3c137b1fbd504d5193e69e6d6ac200d445fe1e2808b04d494395c304b92c0b6878838f43e
4
+ data.tar.gz: aecea712ee39f11a59e245bce3d89136415b3429317500f4b8087c8e3b90b53ca6df937b904ca7f0abfc328f6961016105422ca94a6b4bfd99ad98644758e472
5
+ SHA1:
6
+ metadata.gz: a55261b9a4c1cdf209e6b14c9d04a5a8aa0ff38d
7
+ data.tar.gz: fc27e32b0629998e0aa28180ba01cbb71ae1fc29
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ /spec_database_config.rb*
data/.travis.yml ADDED
@@ -0,0 +1,35 @@
1
+ rvm:
2
+ - "1.8.7"
3
+ - "1.9.3"
4
+ - "2.0.0"
5
+ - "2.1.0"
6
+ - "jruby-1.7.9"
7
+ env:
8
+ - OBJECTID_COLUMNS_AR_TEST_VERSION=3.0.20 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=mysql
9
+ - OBJECTID_COLUMNS_AR_TEST_VERSION=3.0.20 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=postgres
10
+ - OBJECTID_COLUMNS_AR_TEST_VERSION=3.0.20 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=sqlite
11
+ - OBJECTID_COLUMNS_AR_TEST_VERSION=3.1.12 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=mysql
12
+ - OBJECTID_COLUMNS_AR_TEST_VERSION=3.1.12 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=postgres
13
+ - OBJECTID_COLUMNS_AR_TEST_VERSION=3.1.12 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=sqlite
14
+ - OBJECTID_COLUMNS_AR_TEST_VERSION=3.2.16 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=mysql
15
+ - OBJECTID_COLUMNS_AR_TEST_VERSION=3.2.16 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=postgres
16
+ - OBJECTID_COLUMNS_AR_TEST_VERSION=3.2.16 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=sqlite
17
+ - OBJECTID_COLUMNS_AR_TEST_VERSION=4.0.2 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=mysql
18
+ - OBJECTID_COLUMNS_AR_TEST_VERSION=4.0.2 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=postgres
19
+ - OBJECTID_COLUMNS_AR_TEST_VERSION=4.0.2 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=sqlite
20
+ before_script:
21
+ - mysql -e 'create database myapp_test;'
22
+ - psql -c 'create database myapp_test;' -U postgres
23
+ matrix:
24
+ exclude:
25
+ # ActiveRecord 4.x doesn't support Ruby 1.8.7
26
+ - rvm: 1.8.7
27
+ env: OBJECTID_COLUMNS_AR_TEST_VERSION=4.0.2 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=mysql
28
+ - rvm: 1.8.7
29
+ env: OBJECTID_COLUMNS_AR_TEST_VERSION=4.0.2 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=postgres
30
+ - rvm: 1.8.7
31
+ env: OBJECTID_COLUMNS_AR_TEST_VERSION=4.0.2 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=sqlite
32
+ allow_failures:
33
+ - env: OBJECTID_COLUMNS_AR_TEST_VERSION=3.1.12 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=sqlite
34
+ - rvm: 1.8.7
35
+ env: OBJECTID_COLUMNS_AR_TEST_VERSION=3.2.16 OBJECTID_COLUMNS_TRAVIS_CI_DATABASE_TYPE=sqlite
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in objectid_columns.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Swiftype, Inc.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,224 @@
1
+ # ObjectidColumns
2
+
3
+ Transparely store MongoDB [ObjectId](http://docs.mongodb.org/manual/reference/object-id/) values (the primary key of
4
+ all MongoDB tables) in ActiveRecord objects in a relational database. You can store values as:
5
+
6
+ * A binary column (`BINARY`, `VARBINARY`, `BLOB`, _etc._) of length at least 12; the ObjectId value will be stored as
7
+ pure binary data — the most efficient format. (Most databases, including MySQL and PostgreSQL, can index this
8
+ column and work with it just like a String; it just takes half as much space (!).)
9
+ * A String column (`CHAR`, `VARCHAR`, _etc._) of length at least 24; the ObjectId value will be stored as a hexadecimal
10
+ string.
11
+
12
+ (Note that it is not possible to store binary data transparently in a String column, because not all byte sequences
13
+ are valid binary data in all possible character sets.)
14
+
15
+ Once declared, an ObjectId column will return instances of either `BSON::ObjectId` or `Moped::BSON::ObjectId`
16
+ (depending on which one you have loaded) when you access an attribute of a model that you've declared as an ObjectId
17
+ column. It will accept a String (in either hex or binary formats) or an instance of either of those classes when
18
+ assigning to the column.
19
+
20
+ `ObjectidColumns` also allows you to use ObjectId values as primary keys; it will assign them to new records by
21
+ default, and make sure `find(id)` accepts them.
22
+
23
+ This gem requires either the `moped` gem (which defines `Moped::BSON::ObjectId`) or the `bson` gem (which defines
24
+ `BSON::ObjectId`) for the actual ObjectId classes it uses. It declares an official dependency on neither, because we
25
+ want to allow you to use either one. It will accept either one when assigning ObjectIds; it will return ObjectIds as
26
+ whichever one you have loaded, (currently) preferring `BSON::ObjectId` if you have both.
27
+
28
+ ObjectidColumns supports Ruby 1.8.7, 1.9.3, 2.0.0, and 2.1.0, plus JRuby 1.7.9; it supports ActiveRecord 3.0.20,
29
+ 3.1.12, 3.2.16, and 4.0.2. It supports SQLite 3.x, MySQL 5.x, and PostgreSQL 8.x. (These are just the versions it's
30
+ tested against; while it will not work with ActiveRecord 2.x, it is otherwise highly unlikely to be sensitive to
31
+ exact ActiveRecord or Ruby versions, or type of RDBMS, and generally should work with most combinations.)
32
+
33
+ *Note*: If you use SQLite3 with ActiveRecord 3.1.x on MRI (_i.e._, not JRuby), or SQLite3 with ActiveRecord 3.2.x on
34
+ MRI 1.8.7, there is a bug in the SQLite3-ActiveRecord integration that causes ObjectId primary keys to not work. (They
35
+ are inserted properly, but searching by primary key fails.) Needless to say, these are very rare combinations of
36
+ software to be running in production, but it's worth noting. (ActiveRecord 3.0.x or 3.2.x work fine with SQLite3 on
37
+ MRI; SQLite3 with ActiveRecord 3.2.x works fine on MRI >= 1.9.3; and MySQL and PostgreSQL work perfectly everywhere.)
38
+
39
+ Current build status: ![Current Build Status](https://api.travis-ci.org/swiftype/objectid_columns.png?branch=master)
40
+
41
+ Brought to you by the folks at [Swiftype](https://www.swiftype.com). First version written by [Andrew Geweke](https://www.github.com/ageweke).
42
+
43
+ ## Installation
44
+
45
+ First, make sure you have either `BSON::ObjectId` or `Moped::BSON::ObjectId` defined in your Rails environment;
46
+ these are the two ObjectId classes that `ObjectidColumns` can work with. If you don't have either defined, add one
47
+ of these lines to your `Gemfile` (loading both is fine, but unnecessary):
48
+
49
+ gem 'bson'
50
+ OR gem 'moped'
51
+
52
+ Now, add this line to your application's Gemfile:
53
+
54
+ gem 'objectid_columns'
55
+
56
+ And then execute:
57
+
58
+ $ bundle
59
+
60
+ Or install it yourself as:
61
+
62
+ $ gem install objectid_columns
63
+
64
+ ## Usage
65
+
66
+ If you name your object-ID columns with `_oid` at the end, simply do this:
67
+
68
+ class MyModel < ActiveRecord::Base
69
+ has_objectid_columns
70
+ end
71
+
72
+ This will automatically find any columns that end in `_oid` and make them ObjectId columns. When reading them, you will
73
+ get back an instance of an ObjectId class (from `moped` or `bson`, depending on which one you have loaded; see the
74
+ introduction for more). When writing them, you can assign a String in hex or binary formats, or an instance of either
75
+ of the supported ObjectId classes.
76
+
77
+ If you didn't name your columns this way, or _don't_ want to pick up columns ending in `_oid`, just name them
78
+ explicitly:
79
+
80
+ class MyModel < ActiveRecord::Base
81
+ has_objectid_columns :some_oid, :foo
82
+ end
83
+
84
+ This will not only define `some_oid` and `foo` as being ObjectId columns, but it will also skip the automatic detection
85
+ of columns ending in `_oid`.
86
+
87
+ ObjectidColumns will never automatically make a primary-key column an ObjectId, even if it ends with `_oid`; if you
88
+ want a primary key to be an ObjectId, you must do that explicitly, using `has_objectid_primary_key`, below.
89
+
90
+ Note that trying to declare a column as an ObjectId column if it isn't of a supported type (a type that ActiveRecord
91
+ considers to be `:string` or `:binary`), or if it isn't long enough to support an ObjectId (twelve characters for
92
+ binary columns, 24 for string columns); this will happen from the `has_objectid_columns` call (so at load time for the
93
+ model class).
94
+
95
+ Once you have declared such a column:
96
+
97
+ my_model = MyModel.find(...)
98
+
99
+ my_model.my_oid # => BSON::ObjectId('52eab2cf78161f1314000001')
100
+ my_model.my_oid.to_s # => "52eab2cf78161f1314000001" (built-in behavior from BSON::ObjectId)
101
+ my_model.my_oid.to_binary # => "R\xEA\xB2\xCFx\x16\x1F\x13\x14\x00\x00\x01"
102
+ my_model.my_oid.to_binary.encoding # => #<Encoding:ASCII-8BIT>
103
+
104
+ my_model.my_oid = BSON::ObjectId.new # OK
105
+ my_model.my_oid = "52eab32878161f1314000002" # OK
106
+ my_model.my_oid = "R\xEA\xB2\xCFx\x16\x1F\x13\x14\x00\x00\x01" # OK
107
+
108
+ MyModel.where(:my_oid => some_oid).first # => my_model -- i.e., where(...) works with a hash.
109
+
110
+ Note that to assign a binary-format string, it must have an encoding of `Encoding::BINARY` (which is an alias for
111
+ `Encoding::ASCII-8BIT`). (If your string has a different encoding, it may be coming from a source that does not
112
+ actually support full binary data transparently, which _will_ cause big problems.)
113
+
114
+ ### Gotchas
115
+
116
+ If you query on an ObjectId column and use the hash syntax &mdash; _i.e._, `MyModel.where(:some_oid => ...)` &mdash;
117
+ then everything will work perfectly; `ObjectidColumns` looks for this syntax and properly translates the value from
118
+ an ObjectId object, binary String, or hex String to the proper database value. On the other hand, if you specify a
119
+ SQL statement or fragment of SQL yourself, you must do the translation, or you'll either get a database error or just
120
+ no rows found:
121
+
122
+ MyModel.where("some_oid = ?", my_oid.to_bson_id.to_binary) # if some_oid is binary
123
+ MyModel.where("some_oid = ?", my_oid.to_bson_id.to_s) # if some_oid is a String
124
+
125
+ (Without actively parsing SQL, which is kind of an insane thing to do, there is no easy way around this. Even if we
126
+ detected ObjectId objects in the set of values passed in, we'd have no way of figuring out which ObjectId column they
127
+ were constraining on, and thus whether to turn them into binary or hexadecimal ObjectId values.)
128
+
129
+ ### Using an ObjectId for a Primary Key
130
+
131
+ You can use ObjectId values as primary keys:
132
+
133
+ class MyModel < ActiveRecord::Base
134
+ has_objectid_primary_key
135
+ end
136
+
137
+ Perhaps obviously, your primary-key column must either be `:binary` of length >= 12 or `:string` of length >= 24.
138
+
139
+ If your primary-key column is not called `:id`, you must do one of two things:
140
+
141
+ * Set the primary key on the class _before_ you call `has_objectid_primary_key`, by just using the normal ActiveRecord
142
+ `self.primary_key = :foo` syntax.
143
+ * Pass the primary key as an argument to `has_objectid_primary_key`: `has_objectid_primary_key :foo`.
144
+
145
+ (The two are exactly equivalent.)
146
+
147
+ When you do this, `ObjectidColumns` adds a `before_create` hook to your model, assigning a new ObjectId immediately
148
+ before the first time a row is saved in the database. This will not replace an existing ID, so, if you always (or
149
+ sometimes) assign a new ID yourself, `ObjectidColumns` will not overwrite your ID.
150
+
151
+ Perhaps obviously, this means that new IDs are generated and stored client-side; you almost certainly should declare
152
+ your primary-key column as `NOT NULL`, but you don't need to give it a default (and probably shouldn't, unless you're
153
+ using a database function that is capable of generating correctly-formatted ObjectId values using the correct
154
+ algorithm).
155
+
156
+
157
+ ### Setting the Preferred Class
158
+
159
+ If you have both the `bson` and `moped` gems defined, then, by default, ObjectId columns will be returned as instances
160
+ of `bson`'s `BSON::ObjectId` class. If you want to use `moped`'s instead, do this:
161
+
162
+ ObjectidColumns.preferred_bson_class = Moped::BSON::ObjectId
163
+
164
+ ### Extensions
165
+
166
+ This gem extends String with a single method, `#to_bson_id`; it simply returns an instance of the preferred BSON class
167
+ from that String if it's in either the valid hex or the valid binary format, or raises `ArgumentError` otherwise.
168
+
169
+ This gem also extends whatever BSON ObjectId classes are loaded with methods `to_bson_id` (which just returns `self`),
170
+ and the method `to_binary`, which returns a binary String of length 12 for that object ID.
171
+
172
+ ### Running Specs
173
+
174
+ `objectid_columns` has thorough system-level (_i.e._, integration) tests, written in RSpec. Because nearly all of its
175
+ functionality is centered around interfacing with ActiveRecord (as opposed to having significant, complex code within
176
+ its codebase directly), there are no unit tests &mdash; they would simply be setting complex expectations around calls
177
+ to ActiveRecord, making the tests fragile and not particularly useful.
178
+
179
+ In order to run these specs, you must have access to a database you can use. It's best if the database is dedicated
180
+ to running these specs. The tests create and destroy their own tables, and make every effort to clean up anything they
181
+ created at the end &mdash; so it should be possible to piggyback on top of an existing database you also use for other
182
+ things. However, it's _always_ much safer to use a dedicated database. (Note that this is intentional use of the word
183
+ "database", as opposed to "database server"; you don't need, for example, an entirely separate instance of `mysqld`
184
+ &mdash; just a separate database that you can switch to using `USE ....`.)
185
+
186
+ Once you have this set up, simply create a file at the root level of the Gem (_i.e._, inside the root
187
+ `objectid_columns` directory) called `spec_database_config.rb`, and define a constant
188
+ `OBJECTID_COLUMNS_SPEC_DATABASE_CONFIG` as so:
189
+
190
+ OBJECTID_COLUMNS_SPEC_DATABASE_CONFIG = {
191
+ :database_gem_name => 'mysql2',
192
+ :require => 'mysql2',
193
+ :config => {
194
+ :adapter => 'mysql2',
195
+ :database => 'objectid_columns_specs_db',
196
+ :username => 'root'
197
+ }
198
+ }
199
+
200
+ The keys are as follows:
201
+
202
+ * `:database_gem_name` is the name of the RubyGem that provides access to the database &mdash; exactly as you'd put
203
+ it in a `Gemfile`;
204
+ * `:require` is whatever should be passed to Ruby's built-in `require` statement to require the Gem &mdash;
205
+ typically this is the same as `:database_gem_name`, but not always (this is the same as a Gemfile's `:require => ..
206
+ ` syntax);
207
+ * `:config` is exactly what gets passed to `ActiveRecord::Base.establish_connection`, and so you can pass any
208
+ options that it accepts (which are the same as what goes in Rails' `database.yml`).
209
+
210
+ Once you've done this, you can run the system specs using `bundle exec rspec spec/objectid_columns/system`. (Or run
211
+ them along with the unit specs with a simple `bundle exec rspec spec`.)
212
+
213
+ Note that there's also support deep in the code (in
214
+ `objectid_columns/spec/objectid_columns/helpers/database_helper.rb`)
215
+ for defining connections to [Travis CI](https://travis-ci.org/)'s database options, so that Travis can run the tests
216
+ automatically. Generally, you don't need to worry about this, but it's worth noting.
217
+
218
+ ## Contributing
219
+
220
+ 1. Fork it ( http://github.com/swiftype/objectid_columns/fork )
221
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
222
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
223
+ 4. Push to the branch (`git push origin my-new-feature`)
224
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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,104 @@
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
+ if @target_class.const_defined?(@name)
48
+ existing = @target_class.const_get(@name)
49
+
50
+ if existing && existing != self
51
+ raise NameError, %{You tried to define a #{self.class.name} named #{name.inspect} on class #{target_class.name},
52
+ but that class already has a constant named #{name.inspect}: #{existing.inspect}}
53
+ end
54
+ end
55
+
56
+ @class_methods_module = Module.new
57
+ (class << @class_methods_module; self; end).send(:public, :private)
58
+ @target_class.const_set("#{@name}ClassMethods", @class_methods_module)
59
+ @target_class.send(:extend, @class_methods_module)
60
+
61
+ @target_class.const_set(@name, self)
62
+ @target_class.send(:include, self)
63
+
64
+ @methods_defined = { }
65
+ @class_methods_defined = { }
66
+
67
+ super(&block)
68
+ end
69
+
70
+ # Removes all methods that have been defined on this module using #define_method, below. (If you use some other
71
+ # mechanism to define a method on this DynamicMethodsModule, then it will not be removed when this method is
72
+ # called.)
73
+ def remove_all_methods!
74
+ instance_methods.each do |method_name|
75
+ # Important -- we use Class#remove_method, not Class#undef_method, which does something that's different in
76
+ # some important ways.
77
+ remove_method(method_name) if @methods_defined[method_name.to_sym]
78
+ end
79
+
80
+ @class_methods_module.instance_methods.each do |method_name|
81
+ @class_methods_module.send(:remove_method, method_name) if @class_methods_defined[method_name]
82
+ end
83
+ end
84
+
85
+ # Defines a method. Works identically to Module#define_method, except that it's +public+ and #remove_all_methods!
86
+ # will remove the method.
87
+ def define_method(name, &block)
88
+ name = name.to_sym
89
+ super(name, &block)
90
+ @methods_defined[name] = true
91
+ end
92
+
93
+ # Defines a class method.
94
+ def define_class_method(name, &block)
95
+ @class_methods_module.send(:define_method, name, &block)
96
+ end
97
+
98
+ # Makes it so you can say, for example:
99
+ #
100
+ # my_dynamic_methods_module.define_method(:foo) { ... }
101
+ # my_dynamic_methods_module.private(:foo)
102
+ public :private # teehee
103
+ end
104
+ end