Project

dyna_mo

0.0
No commit activity in last 3 years
No release in over 3 years
Dynamic scope implementation for Method Overriding: override methods by specified context, only in current thread
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

 Project Readme

DynaMo: Testing module to provide dynamic scope method overriding

Dynamic scope implementation for Method Overriding: override methods by specified context, only in current thread.

DON'T USE THIS GEM IN PRODUCTION CODE.

What's this?

To modify methods' behavior with its dynamic contexts, like this:

require 'dyna_mo'
 
module MyModule
  class MyClass
    attr_accessor :num
    def initialize; @num = 0; end
    def name; "name"; end
    def sum(numbers); @num + numbers.reduce(:+); end
  end
end
 
dynamo_define('MyModule::MyClass', :mytest_case_default) do
  def_method(:initialize) do
    @num = 1
  end
  
  def_method(:name) do
    "dummyname"
  end
  
  def_instance_method(:name, :mytest_case1) do
    "dummyname1"
  end
  
  def_method(:sum) do |numbers|
    @num + numbers.reduce(:+) + 1
  end
  
  def_class_method(:create) do |init_num=0|
    obj = self.new
    obj.num = init_num
    obj
  end
end
 
class SynopsisTest < Test::Unit::TestCase
  def test_synopsis
    assert { MyModule::MyClass.new.name == "name" }

    obj = MyModule::MyClass.new
    
    assert { obj.num == 0 }
    assert { obj.name == "name" }
    
    dynamo_context(:mytest_case_default) do
      assert { obj.num == 0 } # #initialize is not overridden
      
      assert { obj.name == "dummyname" }
      assert { MyModule::MyClass.new.name == "dummyname" }
      
      assert { MyModule::MyClass.new.num == 1 }
      
      assert { MyModule::MyClass.new.sum([1,2,3]) == (1+(1+2+3)+1) }
      
      assert { MyModule::MyClass.create(100).num == 100 }
    end
    
    dynamo_context(:mytest_case1) do
      assert { obj.name == "dummyname1" }
    end
    
    dynamo_define(MyModule::MyClass, :onetime_context) do
      def_method(:name) do
        "onetime"
      end
    end
    
    dynamo_context(:onetime_context) do
      assert { obj.name == "onetime" }
    end
  end
end
 
module MyModule; class MyClass2 < MyClass; end; end
 
class Synopsis2Test < Test::Unit::TestCase
  def test_onece_more
    dynamo_define(MyModule::MyClass, :onetime_context) do
      def_method(:name) do
        "onetime"
      end
    end
    
    dynamo_context(:onetime_context) do
      assert { MyModule::MyClass2.new.name == "onetime" }
    end
  end
end

Installation

Add this line to your application's Gemfile:

gem 'dyna_mo'

And then execute:

$ bundle

Or install it yourself as:

$ gem install dyna_mo

Usage

Require this module in helper.rb or anywhere you want in test code.

require "dyna_mo"

Kernel.dynamo_define(name_or_module_instance, default_context_name, &block)

Create context to define instance methods and class methods of specified Module/Class.

  • name_or_module_instance accepts both of String or Module/Class instance. But string specification must be name from top-level, like Net::HTTP (or ::Net::HTTP).

Given block is evaluated with a receiver instance of DynaMo::Contexts.

dynamo_define(MyClass, :test_awesome_situation) do
  # ...
end

DynaMo::Contexts#def_instance_method(method_name, context_name = default_context_name, &block)

Define instance method to override existing instance method, only in specified context. Instance variable reference like @data is handled correctly for each objects.

super cannot be used in this block. Use dynamo_super() instead.

Given block is to be method body, and prepended on specified Module/Class, not rewrite method itself. So we can call original method definition by dynamo_super().

 # in dynamo_define
dynamo_instance_method(:data, :my_test_context) do |num|
  @data * num
end
obj.instance_eval{ @data = "abc" }
obj.data(3) #=> "abcabcabc"

The most recently called #def_instance_method have highest priority. It's for ad-hoc definition in test code.

With def_instance_method, given block can have arguments of arbitrary number, not same with original definition. But it brings very confusing behavior (especially for dynamo_super), so it is not recommended for many cases.

This method also can add method which does NOT exists in original Module/Class definition.

DynaMo::Contexts#def_method(...)

Alias of def_instance_method.

DynaMo::Contexts#def_class_method(method_name, context_name = default_context_name, &block)

Define class method. All other things are same with define_instance_method.

Kernel.dynamo_context(context_name, &block)

Create dynamic scope in current thread for specified context name. Given block and lower stack calls run with overridden methods.

require "dyna_mo"
 
class A1; def self.label; "A"; end; end
class A2 < A1; def self.label; (super) * 2; end; end
 
dynamo_define(A1, :test) do
  def_class_method(:label) do
    "AB"
  end
end
 
dynamo_context(:test) do
  A2.label #=> "ABAB"
  Thread.new { A2.label }.value #=> "AA"
end

We can apply 2 or more contexts at the same time. If these contexts have definition for same method, the recent defined one is called at first.

dynamo_define(A1, :test1) do
  def_class_method(:label) do
    "AB"
  end
  def_class_method(:label, :test2) do
    "AC"
  end
  def_class_method(:label, :test3) do
    "BC"
  end
end
 
dynamo_context(:test2) do
  dynamo_context(:test3) do
    dynamo_context(:test1) do
      A2.label #=> "BC"
    end
  end
end

Kernel.dynamo_super(*args)

Use to call original method definition in overriding method body (block). dyna_mo method overriding is implemented with Module.prepend, so dynamo_super() calls original definition, not parent class's definition.

With dynamo_super, all arguments must be specified explicitly.

dynamo_define(A1, :test_default) do
  def_method(:name) do |prefix|
    prefix + dynamo_super()
  end
end

If you use this method with applying multi contexts, dynamo_super() calls each overriding method bodies, like this:

dynamo_define(A1, :test1) do
  def_class_method(:label) do
    "1" + dynamo_super()
  end
  def_class_method(:label, :test2) do
    "2" + dynamo_super()
  end
  def_class_method(:label, :test3) do
    "3" + dynamo_super()
  end
end

dynamo_context(:test1) do
  dynamo_context(:test2) do
    dynamo_context(:test3) do
      # A1.label/test3 -> A1.label/test2 -> A1.label/test1 -> A1.label/(original)
      A1.label  #=> "321A"
    end
  end
end

Contributing

  1. Fork it ( https://github.com/[my-github-username]/dyna_mo/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Copyright

  • License
  • MIT
  • Author
  • @tagomoris