Module: Metamorphosis

Extended by:
Metamorphosis
Included in:
Metamorphosis
Defined in:
lib/metamorphosis/core.rb,
lib/metamorphosis/helpers.rb,
lib/metamorphosis/version.rb

Overview

This module, the main one, is responsible for all hooks setup and extension mechanisms. Therefore, if you have a module or a class which would benefit from Metamorphosis features, just extend Metamorphosis:

MyProject.extend Metamorphosis

Obviously this may look more like this in real-code:

module MyProject
  extend Metamorphosis
end

By extending Metamorphosis, MyModule is able to call activate and a bunch of attr_reader as class methods:

  • receiver (would return MyProject constant, ie. self)
  • base_path (Pathname path of the directory of the file where MyProject extended Metamorphosis)
  • spells_path (Pathname path of the spells root directory)
  • redefinable (list of all MyProject's modules and classes spells may be defined against, ie. the public API from Metamorphosis standing point)
  • spells (list of all activated spells for MyProject).

Be aware of the fact that, since Metamorphosis is extended, the receiver does not gain Metamorphosis class methods, which are part of the private API somehow. They are documented nontheless so as to give you some more hints about Metamorphosis internals.

Defined Under Namespace

Modules: RedefInit Classes: MetamorphosisError

Constant Summary

CALLERS_TO_IGNORE =

paths to be ignored when looking for the receiver's path

[
  /\/metamorphosis(\/(core|helpers))?\.rb$/, # all metamorphosis code
  #/\(.*\)/, # any generated code
  /custom_require\.rb$/, # rubygems require hacks
]
MAJOR =
0
MINOR =
1
PATCH =
0
VERSION =
[MAJOR, MINOR, PATCH].join('.')

Class Method Summary (collapse)

Instance Method Summary (collapse)

Class Method Details

+ (Boolean) activate!(spell_name, receiver, *syms)

The activation process really takes place here.

Called by activate which is part of the public API. This method registers hooks between the receiver and the spell, taking general or specific configuration settings into account.

Options Hash (*syms):

  • :retroactive (Boolean) — default: false

    retroaction flag (from activate)

Raises:

  • (LoadError)

    if the spell file does not exist under the expected location

  • (StandardError)

    if the spell definition is invalid

See Also:

  • activate


106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/metamorphosis/core.rb', line 106

def self.activate!(spell_name, receiver, *syms)
  #begin
    options = syms.flatten!.extract_options!

    # TODO: handle camelcased or underscored or capitalized spell name
    spell_name = spell_name.capitalize

    # TODO: read config file (generic or specific)

    # first, load the spell
    begin
      spell_path = Pathname.new(@@spells_path.to_s + "/" + spell_name.downcase)
      require spell_path.to_s
    rescue LoadError => e
      puts e
      abort "You tried to load a spell which does not exist (#{spell_name})."
    end

    # then, fetch the spell const
    begin
      spell = @@receiver.constant("Spells").constant(spell_name)
    rescue => e
      puts e
      abort "Invalid definition for spell \"#{spell_name}\". Please check #{spell_path.to_s + ".rb"}"
    end

    # process what's inside the spell definition
    spell.fetch_nested(recursive: true, only: :modules) do |e|
      #puts "************************"
      #puts "#{e.inspect} (#{receiver_constant_for inner_spell_module_from e.name})"
      #puts "************************"
      if receiver_constant_for inner_spell_module_from e.name
        # this module exists within the receiver, we're heading to redefs

        # let's say e is Receiver::Spells::ASpell::AModule::Nested::Again,
        e = e.name.split("::")[3..-1]
        # now e is AModule::Nested::Again

        # some special cases related to "Convention over Configuration"
        case e.last
        when "InstanceMethods"
          # this module handles redefs for instance methods of e[-2]
          #puts "case: InstanceMethods"
          #puts e.inspect
          #puts @@redefinable

          e_match = receiver_constant_for(e[0..-2].join("::"))
          #puts e_match

          if options[:retroactive]
            ObjectSpace.each_object(e_match) { |x| activate_on_instance x, spell_name, e }
          end

          e_match.extend RedefInit

          unless @@redefinable[e_match][spell] and @@redefinable[e_match][spell].include? :instance_methods
            (@@redefinable[e_match][spell] ||= []) << :instance_methods
          end
          e.pop

          #puts
        when "ClassMethods"
          #puts "case: ClassMethods"
          #puts e.inspect
          #pp @@redefinable

          e_match = receiver_constant_for(e[0..-2].join("::"))
          e.pop
          unless @@redefinable[e_match][spell] and @@redefinable[e_match][spell].include? :class_methods
            (@@redefinable[e_match][spell] ||= []) << :class_methods
          end

          # pending
          #puts
        else
          #puts "case: No smart-convention provided"
          #puts e.inspect
          #puts @@redefinable
          # pending

          e_match = receiver_constant_for(e[0..-1].join("::"))
          #puts e_match.inspect
          unless @@redefinable[e_match][spell] and @@redefinable[e_match][spell].include? :fresh
            (@@redefinable[e_match][spell] ||= []) << :fresh
          end

          #puts
        end

        #puts @@redefinable
        #puts
        #puts
      else
        # this module does not exist within the receiver
        #puts "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Connard"
      end
    end

    @@spells << spell_name

    # TODO: unpack as an alternative to the default hook processing
    #spell.unpack if spell.respond_to?(:unpack)

    return true
  #rescue Exception => msg
    #puts msg
    #return false
  #end
end

+ (Object) activate_on_instance(instance, spell_name, spell_module)

Activate a plugin for a specific instance object.

An instance method accessor may be provided so the receiver can play too.



286
287
288
# File 'lib/metamorphosis/core.rb', line 286

def self.activate_on_instance instance, spell_name, spell_module
  instance.extend receiver_constant_for("Spells").constant(spell_name).constant(spell_module.join("::"))
end

+ (Object) caller_files

Like Kernel#caller but excluding certain magic entries and without line/method information; the resulting array contains filenames only

See Also:



29
30
31
# File 'lib/metamorphosis/helpers.rb', line 29

def self.caller_files
  caller_locations.map { |file,line| file }
end

+ (Object) caller_locations



20
21
22
23
# File 'lib/metamorphosis/helpers.rb', line 20

def self.caller_locations
  caller.map    { |line| line.split(/:(?=\d|in )/)[0,2] }
        .reject { |file, line| CALLERS_TO_IGNORE.any? { |pattern| file =~ pattern } }
end

+ (Object) included(base)

Raises:



60
61
62
# File 'lib/metamorphosis/core.rb', line 60

def self.included base
  raise MetamorphosisError, "Metamorphosis must be extended, not included (for #{base})."
end

+ (String) inner_spell_module_from(name)

Extracts a sub-const string matching a spell module from a valid receiver constant.



269
270
271
272
273
274
275
276
# File 'lib/metamorphosis/core.rb', line 269

def self.inner_spell_module_from name
  begin
    receiver_constant_for name # check validity
    return name.split("::")[3..-1].join("::")
  rescue Exception => msg
    raise ArgumentError, "Invalid receiver constant (#{msg})"
  end
end

+ (Object) receiver_base_path

Returns the full path of the script Metamorphosis has been called from, following the extend Metamorphosis instruction.



36
37
38
# File 'lib/metamorphosis/helpers.rb', line 36

def self.receiver_base_path
  Pathname.new(caller_files.first).realpath.dirname
end

+ (Const?) receiver_constant_for(name, *syms)

Retrieve the constant under the receiver namespace for a given name. Handles nested namespaces. This method does not perform any smart look-up, hence name must be a complete [nested] namespace[s] path under the receiver's domain.

Given the following structure:

module Base
  module Foo
    module Bar
      Class ChunkyBacon
        # ...
      end
    end
  end
end

and calls performed inside +Base+:

Examples:

Valid use-cases


receiver_constant_for("Foo") # => Base::Foo
receiver_constant_for("Foo::Bar") # => Base::Foo::Bar
receiver_constant_for("Base::Foo::Bar::ChunkyBacon") # => Base::Foo::Bar::ChunkyBacon

Invalid use-cases


receiver_constant_for("Bar")
receiver_constant_for("Bar::ChunkyBacon")

Options Hash (*syms):

  • :full_path (Boolean) — default: false

    by default, the method gets rid of conventional namespaces (InstanceMethods and ClassMethods). Setting this option to true disable this behavior.



252
253
254
255
256
257
258
259
260
261
262
# File 'lib/metamorphosis/core.rb', line 252

def self.receiver_constant_for name, *syms
  options = syms.extract_options!
  name = name.join("::") if name.is_a? Array

  begin
    name.gsub!(/::(InstanceMethods|ClassMethods)$/, "") unless options[:full_path]
    @@receiver.constant name
  rescue Exception
    return nil
  end
end

Instance Method Details

- (Boolean) activate(spell_name, *syms)

Activate a spell.

Must be called by the receiver, ie. the module or class which called extend Metamorphosis.

Options Hash (*syms):

  • :retroactive (Boolean) — default: false

    if the spell alters class instances behavior, sets wether the spell should affect already existing instances once activated



53
54
55
# File 'lib/metamorphosis/core.rb', line 53

def activate spell_name, *syms
  return Metamorphosis.activate!(spell_name, self, syms)
end