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.

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.



92
93
94
95
96
97
98
99
100
101
102
103
104
105
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
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
# File 'lib/namebox.rb', line 92

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 eigenclasses of classes
    modules.each do |m|
      @modules |= m.ancestors
      @modules |= class << m; self; end.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 eigenclasses
  eigenclasses = @classes.map { |c| class << c; self; end }

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

  # 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?

          # namebox method; bind instance method to self.
          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).



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

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

  # match the info
  m = c.match(/^(.*?):(\d+)(:in `(.*)')?$/)
  raise "Unexpected caller syntax in \"#{c}\"" 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.



48
49
50
# File 'lib/namebox.rb', line 48

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



40
41
42
# File 'lib/namebox.rb', line 40

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)


70
71
72
73
74
75
76
77
78
79
# File 'lib/namebox.rb', line 70

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.



24
25
26
27
28
29
30
31
32
33
# File 'lib/namebox.rb', line 24

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.



257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/namebox.rb', line 257

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!).



306
307
308
# File 'lib/namebox.rb', line 306

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

#openObject

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



243
244
245
246
247
248
249
250
251
252
253
# File 'lib/namebox.rb', line 243

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)


280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'lib/namebox.rb', line 280

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