sqrbl 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/History.txt +4 -0
  2. data/LICENSE.txt +621 -0
  3. data/README.txt +85 -0
  4. data/Rakefile +32 -0
  5. data/TODO.txt +11 -0
  6. data/bin/sqrbl +8 -0
  7. data/lib/core_exts/object.rb +44 -0
  8. data/lib/core_exts/symbol.rb +16 -0
  9. data/lib/sqrbl/base_migration_writer.rb +52 -0
  10. data/lib/sqrbl/call_stack.rb +21 -0
  11. data/lib/sqrbl/group.rb +42 -0
  12. data/lib/sqrbl/individual_migration_writer.rb +48 -0
  13. data/lib/sqrbl/migration.rb +62 -0
  14. data/lib/sqrbl/mixins/expects_block_with_new.rb +27 -0
  15. data/lib/sqrbl/mixins/has_todos.rb +65 -0
  16. data/lib/sqrbl/mixins/method_missing_delegation.rb +83 -0
  17. data/lib/sqrbl/step.rb +192 -0
  18. data/lib/sqrbl/step_pair.rb +56 -0
  19. data/lib/sqrbl/unified_migration_writer.rb +34 -0
  20. data/lib/sqrbl.rb +83 -0
  21. data/spec/README.txt +4 -0
  22. data/spec/functional/base_migration_writer_spec.rb +12 -0
  23. data/spec/functional/individual_migration_writer_spec.rb +172 -0
  24. data/spec/functional/unified_migration_writer_spec.rb +97 -0
  25. data/spec/spec_helper.rb +31 -0
  26. data/spec/unit/group_spec.rb +85 -0
  27. data/spec/unit/migration_spec.rb +68 -0
  28. data/spec/unit/sqrbl_spec.rb +28 -0
  29. data/spec/unit/step_pair_spec.rb +110 -0
  30. data/spec/unit/step_spec.rb +154 -0
  31. data/tasks/ann.rake +80 -0
  32. data/tasks/bones.rake +20 -0
  33. data/tasks/gem.rake +201 -0
  34. data/tasks/git.rake +40 -0
  35. data/tasks/notes.rake +27 -0
  36. data/tasks/post_load.rake +34 -0
  37. data/tasks/rdoc.rake +51 -0
  38. data/tasks/rubyforge.rake +55 -0
  39. data/tasks/setup.rb +292 -0
  40. data/tasks/spec.rake +54 -0
  41. data/tasks/svn.rake +47 -0
  42. data/tasks/test.rake +40 -0
  43. data/tasks/zentest.rake +36 -0
  44. metadata +115 -0
data/README.txt ADDED
@@ -0,0 +1,85 @@
1
+ == SQrbL: Making database migrations suck less since July 2009!
2
+
3
+ Copyright (c) 2009 raSANTIAGO + Associates LLC (http://www.rasantiago.com)
4
+
5
+ The source code for SQrbL can be found on GitHub at:
6
+ 1. http://github.com/rasantiago/sqrbl/
7
+ 2. http://github.com/geeksam/sqrbl/
8
+
9
+ SQrbL's creator and primary author is Sam Livingston-Gray.
10
+
11
+ == DESCRIPTION:
12
+
13
+ SQrbL was created to help manage an extremely specific problem: managing SQL-based database migrations.
14
+
15
+ In essence, SQrbL is a tool for managing multiple SQL queries using Ruby. SQrbL borrows some terminology and ideas from ActiveRecord's schema migrations, but where ActiveRecord manages changes to your database schema over time, SQrbL was written to manage the process of transforming your data from one schema to another. (Of course, you could use SQrbL for the former case as well -- just use it to write DDL queries -- but ActiveRecord has better tools for figuring out which migrations have already been applied.)
16
+
17
+ ===How It Works:
18
+
19
+ You describe the steps in your migration in a SQrbL file. Each step can produce as much or as little SQL as you like. Each step has an "up" and a "down" part -- so you can do and undo each step as many times as you need to, until you get it just right. When you run your SQrbL file, it creates a tree of *.sql files containing the output from your migration steps, one file per step. It also creates the combined files "all_up.sql" and "all_down.sql"; these contain all of your steps combined into one giant file so that, when Cutover Day arrives, you can copy/paste the whole thing into your SQL client and run it all at once.
20
+
21
+ ===Why It Exists:
22
+
23
+ SQL is a fantastic DSL for describing queries. It's not bad at doing transformations, either. Unfortunately, SQL is, um... rather verbose. And, it lacks tools for reducing duplication -- if you have to run five similar queries that differ only in their parameters, you have to figure out how to use a prepared statement, but if you have to run five similar queries that differ in the field names they use, that's a bit more work. SQrbL lets you use SQL for the things SQL is good at, and Ruby for the other stuff.
24
+
25
+ ===About the Name:
26
+
27
+ SQL.rb seemed a bit too pretentious, so I went with SQrbL -- as in, "You got Ruby in my SQL." I pronounce it "squirble" (rhymes with "squirrel"). The proper capitalization can be annoying to type, so I've aliased the SQrbL class as Sqrbl.
28
+
29
+ == SYNOPSIS:
30
+
31
+ <i>(Note that, in the following code sample, I'm using the convention that do/end blocks are used primarily for their side effects, and curly-brace blocks are used primarily for their return value.)</i>
32
+
33
+ Sqrbl.migration "Convert from old widgets to new widgets" do
34
+ set_output_directory '/path/to/generated/sql'
35
+
36
+ group "Widgets" do
37
+ step "Create widgets" do
38
+ up do
39
+ action "Migrate old_widgets" {
40
+ <<-SQL
41
+ #{
42
+ insert_into("new_widgets", {
43
+ :name => 'widget_name',
44
+ :part_num => 'CONCAT("X_", part_number)',
45
+ :note => '"Imported from old_widgets"',
46
+ })
47
+ }
48
+ FROM old_widgets
49
+ SQL
50
+ }
51
+ end
52
+
53
+ down do
54
+ action "Drop imported organizational contacts" {
55
+ 'DELETE FROM new_widgets WHERE note LIKE "Imported from old_widgets"'
56
+ }
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ The above code sample describes a migration with one step: migrating the data in an `old_widgets` table to a `new_widgets` table. When run, this will generate a set of *.sql files in the directory /path/to/generated/sql.
63
+
64
+ == REQUIREMENTS:
65
+
66
+ * Ruby. (SQrbL was written using MRI Ruby 1.8.6, and has not been tested using other versions.)
67
+
68
+ == INSTALL:
69
+
70
+ sudo gem install rasantiago-sqrbl --source http://gems.github.com
71
+
72
+ == LICENSE:
73
+
74
+ SQrbL is free software: you can redistribute it and/or modify
75
+ it under the terms of the GNU General Public License as published by
76
+ the Free Software Foundation, either version 3 of the License, or
77
+ (at your option) any later version.
78
+
79
+ This program is distributed in the hope that it will be useful,
80
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
81
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
82
+ GNU General Public License for more details.
83
+
84
+ You should have received a copy of the GNU General Public License
85
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ # Look in the tasks/setup.rb file for the various options that can be
2
+ # configured in this Rakefile. The .rake files in the tasks directory
3
+ # are where the options are used.
4
+
5
+ begin
6
+ require 'bones'
7
+ Bones.setup
8
+ rescue LoadError
9
+ begin
10
+ load 'tasks/setup.rb'
11
+ rescue LoadError
12
+ raise RuntimeError, '### please install the "bones" gem ###'
13
+ end
14
+ end
15
+
16
+ ensure_in_path 'lib'
17
+ require 'sqrbl'
18
+
19
+ task :default => 'spec:run'
20
+
21
+ PROJ.rubyforge.name = 'sqrbl'
22
+ PROJ.name = 'sqrbl'
23
+ PROJ.version = Sqrbl::VERSION
24
+ PROJ.authors = 'Sam Livingston-Gray'
25
+ PROJ.email = 'geeksam@gmail.com'
26
+ PROJ.url = 'http://sqrbl.rubyforge.org'
27
+ PROJ.ignore_file = '.gitignore'
28
+
29
+
30
+ PROJ.spec.opts << '--color'
31
+
32
+ # EOF
data/TODO.txt ADDED
@@ -0,0 +1,11 @@
1
+ - bin/sqrbl should create an empty SQrbL file with the following simple template:
2
+ require 'rubygems'
3
+ require 'sqrbl'
4
+ Sqrbl.migration do
5
+ # (copy the rest from README.txt)
6
+ end
7
+
8
+ - Writers should create some sort of output to let you know that something happened.
9
+
10
+ - Define #helper(&block) as a simple wrapper to instance_eval on the same object. (Purpose: allow you to define helpers in a block that can be code-folded.)
11
+
data/bin/sqrbl ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path(
4
+ File.join(File.dirname(__FILE__), %w[.. lib sqrbl]))
5
+
6
+ # Put your code here
7
+
8
+ # EOF
@@ -0,0 +1,44 @@
1
+ # Swiped from Rails' ActiveSupport 2.3.2
2
+
3
+ class Object
4
+ # Returns +value+ after yielding +value+ to the block. This simplifies the
5
+ # process of constructing an object, performing work on the object, and then
6
+ # returning the object from a method. It is a Ruby-ized realization of the K
7
+ # combinator, courtesy of Mikael Brockman.
8
+ #
9
+ # ==== Examples
10
+ #
11
+ # # Without returning
12
+ # def foo
13
+ # values = []
14
+ # values << "bar"
15
+ # values << "baz"
16
+ # return values
17
+ # end
18
+ #
19
+ # foo # => ['bar', 'baz']
20
+ #
21
+ # # returning with a local variable
22
+ # def foo
23
+ # returning values = [] do
24
+ # values << 'bar'
25
+ # values << 'baz'
26
+ # end
27
+ # end
28
+ #
29
+ # foo # => ['bar', 'baz']
30
+ #
31
+ # # returning with a block argument
32
+ # def foo
33
+ # returning [] do |values|
34
+ # values << 'bar'
35
+ # values << 'baz'
36
+ # end
37
+ # end
38
+ #
39
+ # foo # => ['bar', 'baz']
40
+ def returning(value)
41
+ yield(value)
42
+ value
43
+ end
44
+ end
@@ -0,0 +1,16 @@
1
+ # Swiped from Rails' ActiveSupport 2.3.2
2
+
3
+ unless :to_proc.respond_to?(:to_proc)
4
+ class Symbol
5
+ # Turns the symbol into a simple proc, which is especially useful for enumerations. Examples:
6
+ #
7
+ # # The same as people.collect { |p| p.name }
8
+ # people.collect(&:name)
9
+ #
10
+ # # The same as people.select { |p| p.manager? }.collect { |p| p.salary }
11
+ # people.select(&:manager?).collect(&:salary)
12
+ def to_proc
13
+ Proc.new { |*args| args.shift.__send__(self, *args) }
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,52 @@
1
+ require 'fileutils'
2
+
3
+ module Sqrbl
4
+ # Base class for other migration writers.
5
+ class BaseMigrationWriter
6
+ attr_accessor :migration, :output_directory
7
+
8
+ class << self
9
+ # List all classes that inherit from this one
10
+ def subclasses
11
+ @@subclasses ||= []
12
+ end
13
+
14
+ def inherited(subclass) #:nodoc:
15
+ subclasses << subclass
16
+ end
17
+
18
+ # Convenience method: create a new instance and invoke <tt>write!</tt> on it.
19
+ def write_migration!(migration)
20
+ new(migration).write!
21
+ end
22
+ end
23
+
24
+ def initialize(migration) #:nodoc:
25
+ @migration = migration
26
+ set_default_output_dir!
27
+ end
28
+
29
+ protected
30
+ # Set the output_directory attribute based on the output_directory or creating_file
31
+ # attributes of the given migration, in that order
32
+ def set_default_output_dir!
33
+ raise "No migration given!" unless migration
34
+ self.output_directory ||= migration.output_directory
35
+ self.output_directory ||= migration.creating_file && File.join(File.expand_path(File.dirname(migration.creating_file)), 'sql')
36
+ raise "Unable to determine output directory!" unless self.output_directory
37
+ end
38
+
39
+ # Make sure that the given directory exists
40
+ def ensure_dir_exists(dirname)
41
+ raise ArgumentError.new("dirname cannot be nil!") if dirname.nil?
42
+ dirname = File.expand_path(dirname)
43
+ return if File.directory?(dirname)
44
+ FileUtils.makedirs(dirname)
45
+ end
46
+
47
+ # Open +filename+ and write +contents+ to it
48
+ def write_file(filename, contents)
49
+ File.open(filename, 'w') { |f| f << contents }
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,21 @@
1
+ module Sqrbl
2
+ # A small set of tools for working with the call stack.
3
+ module CallStack
4
+ module_function
5
+
6
+ # Find the first item on the call stack that isn't in the Sqrbl library path
7
+ def first_non_sqrbl_caller(call_stack = caller)
8
+ call_stack.detect { |call| ! call.include?(Sqrbl::LIBPATH) }
9
+ end
10
+
11
+ # For a given entry in the call stack, get the file
12
+ def caller_file(caller_item)
13
+ caller_item.split(':')[0]
14
+ end
15
+
16
+ # For a given entry in the call stack, get the line number
17
+ def caller_line(caller_item)
18
+ caller_item.split(':')[1]
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,42 @@
1
+ # This file is part of SQrbL, which is free software licensed under v3 of the GNU GPL.
2
+ # For more information, please see README.txt and LICENSE.txt in the project's root directory.
3
+
4
+
5
+ module Sqrbl
6
+ # Like the Migration class, Group doesn't do much on its own.
7
+ # It's basically a container for a list of StepPair objects, which are created using #step.
8
+ #
9
+ # Group delegates +method_missing+ calls to its +migration+ object.
10
+ # For more information, see MethodMissingDelegation.
11
+ class Group
12
+ attr_reader :migration, :description, :block, :steps
13
+
14
+ include Sqrbl::ExpectsBlockWithNew
15
+ include Sqrbl::MethodMissingDelegation
16
+ delegate_method_missing_to :migration
17
+ include HasTodos
18
+
19
+ def initialize(migration, description, options = {}, &block)
20
+ @migration = migration
21
+ @description = description
22
+ @block = lambda(&block)
23
+ @steps = []
24
+
25
+ eval_block_on_initialize(options)
26
+ end
27
+
28
+ # Creates a StepPair object, passing it the step_description and block arguments.
29
+ def step(step_description, &block)
30
+ steps << StepPair.new(self, step_description, &block)
31
+ end
32
+
33
+ # A Group is valid if it contains at least one StepPair object, and all of those objects are themselves valid.
34
+ def valid?
35
+ !steps.empty? && steps.all? { |step| step.kind_of?(StepPair) && step.valid? }
36
+ end
37
+
38
+ def unix_name
39
+ Sqrbl.calculate_unix_name(description)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,48 @@
1
+ module Sqrbl
2
+ # Writes one [numbered] file per migration step, in two main directories: sql/up/ and sql/down/.
3
+ # Migration steps are contained in a [numbered] subfolder that corresponds to the group.
4
+ class IndividualMigrationWriter < BaseMigrationWriter
5
+ # Prepare the up and down directories and populate them with individual files.
6
+ #
7
+ # WARNING: <b>RECURSIVELY DELETES ALL FILES</b> in output_directory/up and output_directory/down unless passed :safe_mode => true in the options hash!
8
+ def write!(options = {})
9
+ ensure_dir_exists(output_directory)
10
+ clear_dirs!(options)
11
+ write_individual_files!
12
+ end
13
+
14
+ protected
15
+ # Recursively delete all files in output_directory/up and output_directory/down unless passed :safe_mode => true in the options hash.
16
+ def clear_dirs!(options)
17
+ return if options[:safe_mode]
18
+ base_dir = File.expand_path(output_directory)
19
+ FileUtils.rm_rf(File.join(base_dir, 'up'))
20
+ FileUtils.rm_rf(File.join(base_dir, 'down'))
21
+ end
22
+
23
+ # Write out the contents of the individual files, each in its group's subfolder
24
+ def write_individual_files!
25
+ migration.groups.each_with_index do |group, idx|
26
+ group_dir = "%0#{group_num_width}d_%s" % [idx + 1, group.unix_name]
27
+ up_subdir = File.join(output_directory, 'up', group_dir)
28
+ down_subdir = File.join(output_directory, 'down', group_dir)
29
+ ensure_dir_exists(up_subdir)
30
+ ensure_dir_exists(down_subdir)
31
+
32
+ group.steps.each_with_index do |step, idx|
33
+ step_filename = "%0#{step_num_width(step)}d_%s.sql" % [idx + 1, step.unix_name]
34
+ write_file(File.join(up_subdir, step_filename), step.up_step.output )
35
+ write_file(File.join(down_subdir, step_filename), step.down_step.output)
36
+ end
37
+ end
38
+ end
39
+
40
+ def group_num_width # :nodoc:
41
+ migration.groups.length.to_s.length
42
+ end
43
+
44
+ def step_num_width(step) # :nodoc:
45
+ step.group.steps.length.to_s.length
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,62 @@
1
+ # This file is part of SQrbL, which is free software licensed under v3 of the GNU GPL.
2
+ # For more information, please see README.txt and LICENSE.txt in the project's root directory.
3
+
4
+
5
+ module Sqrbl
6
+
7
+ # The Migration class doesn't do much on its own; it's basically just a
8
+ # container for a list of Group objects, which are created using #group.
9
+ #
10
+ # Note also that because Group includes the MethodMissingDelegation mixin,
11
+ # instance methods defined in the block given to Migration.build will become
12
+ # available to all groups (and their related objects) that belong to a
13
+ # Migration instance. For more information, see MethodMissingDelegation.
14
+ class Migration
15
+ attr_reader :groups, :creating_file, :output_directory
16
+
17
+ include HasTodos
18
+
19
+ # Creates a new Migration object and evaluates the block in its binding.
20
+ # As a result, any methods called within the block will affect the state
21
+ # of that object (and that object only).
22
+ #
23
+ # <i>(Eventually, this will also pass the migration to a helper object that
24
+ # will create the tree of output *.sql files.)</i>
25
+ def self.build(&block)
26
+ returning(self.new) do |migration|
27
+ migration.instance_eval(&block)
28
+ end
29
+ end
30
+
31
+ def initialize # :nodoc:
32
+ @groups = []
33
+ creating_caller = CallStack.first_non_sqrbl_caller
34
+ @creating_file = File.expand_path(CallStack.caller_file(creating_caller))
35
+ end
36
+
37
+ # Convenience method: set the +output_directory+ attribute
38
+ def set_output_directory(dirname)
39
+ self.output_directory = File.expand_path(dirname)
40
+ end
41
+
42
+ # Creates a Group object, passing it the name and block arguments.
43
+ def group(name, &block)
44
+ groups << Group.new(self, name, &block)
45
+ end
46
+
47
+ # Convenience method: iterate all StepPair objects in the migration
48
+ def step_pairs
49
+ groups.map(&:steps).flatten
50
+ end
51
+
52
+ # Convenience method: iterate all 'up' steps in the migration
53
+ def up_steps
54
+ step_pairs.map(&:up_step)
55
+ end
56
+
57
+ # Convenience method: iterate all 'down' steps in the migration
58
+ def down_steps
59
+ step_pairs.map(&:down_step)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,27 @@
1
+ # This file is part of SQrbL, which is free software licensed under v3 of the GNU GPL.
2
+ # For more information, please see README.txt and LICENSE.txt in the project's root directory.
3
+
4
+
5
+ module Sqrbl
6
+ # This module primarily exists to help with testing.
7
+ # It encapsulates the common pattern of a method on one object
8
+ # that takes a block, creates a new object, and passes the block
9
+ # to the new object.
10
+ #
11
+ # In normal use, the new object should then immediately instance_eval
12
+ # the block; however, for testing, it is sometimes useful to delay
13
+ # block evaluation until later so we can set up mocking beforehand.
14
+ #
15
+ # (Search the /spec directory for "skip_block_evaluation" for examples.)
16
+ module ExpectsBlockWithNew
17
+ protected
18
+
19
+ def eval_block_on_initialize(options = {})
20
+ evaluate_block! unless options[:skip_block_evaluation]
21
+ end
22
+
23
+ def evaluate_block!
24
+ instance_eval(&block) if block
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,65 @@
1
+ # This file is part of SQrbL, which is free software licensed under v3 of the GNU GPL.
2
+ # For more information, please see README.txt and LICENSE.txt in the project's root directory.
3
+
4
+
5
+ module Sqrbl
6
+
7
+ module HasTodos
8
+ class Todo
9
+ attr_accessor :message, :call_stack, :type
10
+ def initialize(message, call_stack, type)
11
+ @message = message
12
+ @call_stack = call_stack
13
+ @type = type
14
+ end
15
+
16
+ # Returns the first item from the call stack that isn't inside the Sqrbl library.
17
+ # This lets us output a pointer back to the place where this instance was created.
18
+ def location
19
+ @location ||= call_stack.detect { |call| ! call.include?(Sqrbl::LIBPATH) }
20
+ end
21
+
22
+ # Return just the line number from +location+.
23
+ def calling_line
24
+ CallStack.caller_line(location)
25
+ end
26
+
27
+ # Return just the filename from +location+.
28
+ def creating_file
29
+ CallStack.caller_file(location)
30
+ end
31
+
32
+ # Is this Todo of type <tt>:todo</tt>?
33
+ def todo?
34
+ type == :todo
35
+ end
36
+
37
+ # Is this Todo of type <tt>:warning</tt>?
38
+ def warning?
39
+ type == :warning
40
+ end
41
+ end
42
+
43
+ # Return the list of Todo items.
44
+ def todos
45
+ @todos ||= []
46
+ end
47
+
48
+ # Create a new Todo item (of type <tt>:todo</tt>) and add it to +todos+.
49
+ def todo(message)
50
+ add_todo(message, caller, :todo)
51
+ end
52
+
53
+ # Create a new Todo item (of type <tt>:warning</tt>) and add it to +todos+.
54
+ def warning(message)
55
+ add_todo(message, caller, :warning)
56
+ end
57
+
58
+ protected
59
+ def add_todo(message, caller, type)
60
+ returning Todo.new(message, caller, type) do |new_todo|
61
+ todos << new_todo
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,83 @@
1
+ # This file is part of SQrbL, which is free software licensed under v3 of the GNU GPL.
2
+ # For more information, please see README.txt and LICENSE.txt in the project's root directory.
3
+
4
+
5
+ module Sqrbl
6
+ # When used as directed, Sqrbl will create a graph of objects with bidirectional
7
+ # references to one another. The graph will have the following general shape
8
+ # (where --> indicates a relationship that is generally one-to-many):
9
+ #
10
+ # Migration --> Group --> StepPair --> Step
11
+ #
12
+ # Also, because these various objects are created with blocks that are
13
+ # instance_eval'd, those blocks can define helper methods that will be
14
+ # added to the appropriate objects in the graph.
15
+ #
16
+ # Taken together, these two features (back-references and instance_eval)
17
+ # let us provide a useful facility for defining helper methods inside a
18
+ # block, and accessing them using scoping rules that work in an intuitive
19
+ # manner. For example, if a Migration defines a method #foo, which is
20
+ # later called from inside a Step's block, the Step can catch that call
21
+ # using method_missing and delegate it to its StepPair, which in turn
22
+ # will delegate to its Group, which in turn will delegate to its Migration.
23
+ #
24
+ # Confused yet? Here's a slightly modified version of the example in README.txt:
25
+ #
26
+ # Sqrbl.migration "Convert from old widgets to new widgets" do
27
+ # def new_widget_insert()
28
+ # insert_into("new_widgets", {
29
+ # :name => 'widget_name',
30
+ # :part_num => 'CONCAT("X_", part_number)',
31
+ # :note => '"Imported from old_widgets"',
32
+ # })
33
+ # end
34
+ #
35
+ # group "Widgets" do
36
+ # step "Create widgets" do
37
+ # up do
38
+ # action "Migrate old_widgets" {
39
+ # "#{new_widget_insert()} FROM old_widgets"
40
+ # }
41
+ # end
42
+ #
43
+ # down do
44
+ # action "Drop imported organizational contacts" {
45
+ # 'DELETE FROM new_widgets WHERE note LIKE "Imported from old_widgets"'
46
+ # }
47
+ # end
48
+ # end
49
+ # end
50
+ # end
51
+ #
52
+ # Note that the call to new_widget_insert occurs several layers of
53
+ # nesting down from its definition. By using this method_missing
54
+ # delegation strategy, we can effectively hide Sqrbl's object model
55
+ # from the user and provide something that "just works."
56
+ #
57
+ # (Without this strategy, the above example would need to define
58
+ # new_widget_insert as a lambda function that gets invoked using
59
+ # either #call or the square-bracket syntax, but I find that more
60
+ # awkward -- hence this wee bit of metaprogramming.)
61
+ module MethodMissingDelegation
62
+ def self.included(receiver)
63
+ receiver.class_eval(<<-EOF, __FILE__, __LINE__)
64
+ @@mm_delegate_accessor = nil
65
+
66
+ # Defines the accessor method that instances of this class should use
67
+ # when delegating +method_missing+ calls.
68
+ def self.delegate_method_missing_to(accessor)
69
+ @@mm_delegate_accessor = accessor
70
+ end
71
+
72
+ # If +delegate_method_missing_to+ was called on the class,
73
+ # use the accessor defined there to find a delegate and
74
+ # pass the unknown method to it.
75
+ def method_missing(method, *args, &block)
76
+ return super unless defined?(@@mm_delegate_accessor) && !@@mm_delegate_accessor.nil?
77
+ delegate = self.send(@@mm_delegate_accessor)
78
+ delegate.send(method, *args, &block)
79
+ end
80
+ EOF
81
+ end
82
+ end
83
+ end