Class: Namebox

Inherits:
Object
  • Object
show all
Defined in:
lib/namebox.rb

Overview

Namebox for Ruby

© 2013 Sony Fermino dos Santos rubychallenger.blogspot.com.br/2013/01/namebox.html

License: Public Domain

This software is released “AS IS”, without any warranty. The author is not responsible for the consequences of use of this software.

This version is only compatible with Ruby 1.9.2 or greater. For use with Ruby 1.8.7 or 1.9.1, get the version 0.1.8 or 0.1.9.

Defined Under Namespace

Modules: Old

Constant Summary collapse

CORE =

Currently loaded top-level modules

(Module.constants - [:Config]).
map { |c| Object.const_get(c) }.
select { |m| m.is_a? Module }.uniq

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*modules_to_protect, &blk) ⇒ Namebox

modules_to_protect must be the classes themselves, e.g., String, Symbol, not their names (“String” or :Symbol).
Special names are:
:all => protect all known modules and submodules. It’s safer but slower.
:core => protect the known top-level modules.
:default => protect the modules defined in Namebox.default_modules.
Obs.: :core and :default can be used together and/or with other classes, but :all must be the only parameter if it is used.



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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/namebox.rb', line 132

def initialize *modules_to_protect, &blk

  # initialize
  @enabled_files = []
  @protected_methods = []

  # this_nb will be useful in a closure, where self will be different
  this_nb = self

  # by default, get Namebox.default_modules
  modules_to_protect = Namebox.default_modules if modules_to_protect.empty?

  # check for :all modules
  if modules_to_protect.include? :all

    # :all must be the only parameter
    if modules_to_protect.length > 1
      raise "If :all is used in Namebox.new, :all must be the only parameter!"
    end

    # get all modules
    @modules = ObjectSpace.each_object(Module).to_a

  else

    # take off the symbols
    modules = modules_to_protect.select { |m| m.is_a? Module }
    @modules = modules

    # include default modules if wanted
    @modules += Namebox.default_modules if modules_to_protect.include? :default

    # include core modules if wanted
    @modules += CORE if modules_to_protect.include? :core

    # avoid redundancy
    @modules.uniq!

    # include all ancestors for modules and singleton classes of classes
    modules.each do |m|
      @modules |= m.ancestors
      @modules |= m.singleton_class.ancestors if m.is_a? Class
    end
  end

  # modules must be given or Namebox.default_modules must be set
  if @modules.empty?
    raise ("Modules to protect were not given and there's no " +
          "Namebox.default_modules defined for file " +
          Namebox.caller_info[:file])
  end

  # select classes to protect against included modules
  @classes = @modules.select { |m| m.is_a? Class }

  # get singleton_classes
  singleton_classes = @classes.map { |c| c.singleton_class }

  # include singleton_classes into @classes and @modules
  @classes |= singleton_classes
  @modules |= singleton_classes

  # permits to call the previous (_old) version of the method.
  @modules.each { |m| m.send(:include, Namebox::Old) unless m == Namebox::Old }

  # save preexisting methods and included modules
  inc_mods_before = get_included_modules
  methods_before = get_methods

  # ###############################################################
  # RUN THE CODE, which can change the methods of protected modules
  #
  blk.call
  #
  # ###############################################################

  # get data after changes
  inc_mods_after = get_included_modules
  methods_after = get_methods

  # compare with preexisting data to discover affected methods and modules

  # compare included modules (before vs after)
  unless inc_mods_after == inc_mods_before

    inc_mods_after.each do |klass, inc_mods|

      old_modules = inc_mods_before[klass]
      new_modules = inc_mods - old_modules

      # there's no new included module for this class
      next if new_modules.empty?

      new_modules.each do |new_module|

        # Get a protector module for new_module; don't recreate it
        # if a protector was already created for new_module.
        #
        protector = protector_module(new_module)

        # reincludes the new_module with super_tunnel
        # to allow bind(self) inside protector code.
        #
        klass.send :include, new_module

        # finally, include the protector in the class
        klass.send :include, protector

      end
    end
  end

  # Compare changed methods (before vs after)
  unless methods_after == methods_before

    methods_after.each do |fullname, info|

      # get old method
      info_old = methods_before[fullname] || {}
      new_method = info[:method]
      old_method = info_old[:method]

      # don't touch unmodified methods
      next if new_method == old_method

      # method was modified! take some info
      method_name = info[:name]
      klass = info[:class]

      # redefine the method, which will check namebox visibility dinamically
      klass.send :define_method, method_name do |*args, &blk|

        # check namebox visibility
        if this_nb.open? && !_calling_old?(fullname)

          # namebox method; bind instance method to self.
          r = new_method.bind(self).call(*args, &blk)

        else

          # old method or super
          if old_method
            old_method.bind(self).call(*args, &blk)
          else
            super(*args, &blk)
          end

        end

      end

    end
  end
end

Class Method Details

.caller_infoObject

Get the caller info in a structured way (hash).



86
87
88
89
90
91
92
93
94
95
96
# File 'lib/namebox.rb', line 86

def caller_info

  # search for last reference to this file in caller and take the next one
  cr = caller.reverse
  c = cr[cr.index { |s| s.start_with? __FILE__ } - 1]
  raise "Unable to find a valid caller file in #{caller.inspect}" unless c

  # convert into hash
  caller_to_hash c

end

.caller_to_hash(a_caller) ⇒ Object



110
111
112
113
114
115
116
117
118
119
# File 'lib/namebox.rb', line 110

def caller_to_hash a_caller

  # match the info
  m = a_caller.match(/^(.*?):(\d+)(:in `(.*)')?$/)
  raise "Unexpected caller syntax in \"#{a_caller}\"" unless m

  # label them
  {:file => m[1], :line => m[2].to_i, :method => m[4]}

end

.default_modulesObject

Array with default modules to protect. You may want redefine this, protecting Namebox itself inside another namebox, and assign it to a constant, which can be available thru other files in the project.



81
82
83
# File 'lib/namebox.rb', line 81

def default_modules
  (@default_modules ||= {})[caller_info[:file]] || []
end

.default_modules=(modules_to_protect) ⇒ Object

Set the default modules to protect (file-wide, for the caller file). Each file you want to use #default_modules, you must define them again. This is to avoid conflicts when using other people libraries, which may want to protect different modules by default. See #default_modules



73
74
75
# File 'lib/namebox.rb', line 73

def default_modules= modules_to_protect
  (@default_modules ||= {})[caller_info[:file]] = [modules_to_protect].flatten
end

.no_method_error(obj, m_name) ⇒ Object

Raises NoMethodError, limiting the length of obj.inspect.

Raises:

  • (NoMethodError)


99
100
101
102
103
104
105
106
107
108
# File 'lib/namebox.rb', line 99

def no_method_error(obj, m_name)

  # if inspect is too big, shorten it
  obj_name = obj.inspect.to_s
  obj_name = obj_name[0..45] + '...' + obj_name[-1] if obj_name.length > 50

  msg = "Undefined method `#{m_name}' for #{obj_name}:#{obj.class}"

  raise NoMethodError.new(msg)
end

.require(resource, *modules_to_protect) ⇒ Object

Wrapper to create a namebox only to protect modules when requiring.



57
58
59
60
61
62
63
64
65
66
# File 'lib/namebox.rb', line 57

def require resource, *modules_to_protect

  new(*modules_to_protect) do

    # need to refer to top-level binding, which is lost inside this def.
    TOPLEVEL_BINDING.eval("require '#{resource}'")

  end

end

Instance Method Details

#closeObject

Close the namebox visible region in the caller file.



302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/namebox.rb', line 302

def close
  info = ranges_info

  # there must be an open range in progress
  unless info[:last]
    raise "Namebox was not opened in #{info[:file]} before line #{info[:line]}"
  end

  # begin of range must be before end
  r_beg = info[:last]
  r_end = info[:line]
  unless r_end >= r_beg
    raise ("Namebox#close in #{info[:file]}:#{r_end} should be after " +
          "Namebox#open (line #{r_beg})")
  end

  # replace the single initial line with the range, making sure it's unique
  r = Range.new(r_beg, r_end)
  info[:ranges].pop
  info[:ranges] << r unless info[:ranges].include? r
end

#file_wideObject

Open namebox in entire caller file (valid only after called!).



351
352
353
# File 'lib/namebox.rb', line 351

def file_wide
  @enabled_files << Namebox.caller_info[:file]
end

#openObject

Open a namebox region for visibility in the caller file at caller line.



288
289
290
291
292
293
294
295
296
297
298
# File 'lib/namebox.rb', line 288

def open
  info = ranges_info

  # there must be no open range
  if info[:last]
    raise "Namebox was already opened in #{info[:file]}:#{info[:last]}"
  end

  # range in progress
  info[:ranges] << info[:line]
end

#open?Boolean

Check namebox visibility (openness).

Returns:

  • (Boolean)


325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
# File 'lib/namebox.rb', line 325

def open?

  # check file before checking ranges
  ci = Namebox.caller_info
  return true if @enabled_files.include? ci[:file]

  # check ranges for this file
  info = ranges_info ci
  info[:ranges].each do |r|
    case r
    when Range

      # check if caller is in an open range for this namebox
      return true if r.include?(info[:line])

    when Integer

      # check if caller is after an initied range (Namebox#open)
      return true if info[:line] >= r

    end
  end
  false
end