namebox 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. data/lib/namebox.rb +258 -0
  2. metadata +47 -0
@@ -0,0 +1,258 @@
1
+ # Namebox for Ruby
2
+ #
3
+ # (c) 2012 Sony Fermino dos Santos
4
+ # http://rubychallenger.blogspot.com.br/2013/01/namebox.html
5
+ #
6
+ # License: Public Domain
7
+ # This software is released "AS IS", without any warranty.
8
+ # The author is not responsible for the consequences of use of this software.
9
+ #
10
+ # This code is not intended to look professional,
11
+ # provided that it does what it is supposed to do.
12
+
13
+ class Namebox
14
+
15
+ CORE = (Module.constants - [:Config]).map { |c| Object.const_get(c) }.select { |m| m.is_a? Module }.uniq
16
+
17
+ class << self
18
+
19
+ # Get the caller info in a structured way (hash)
20
+ def caller_info
21
+ # search for last reference to this file in caller and take the next one
22
+ cr = caller.reverse
23
+ c = cr[cr.find_index { |s| s.start_with? __FILE__ } - 1]
24
+ raise "Unable to find a valid caller file in #{caller.inspect}" unless c
25
+
26
+ m = c.match(/^(.*?):(\d+)(:in `(.*)')?$/)
27
+ raise "Unexpected caller syntax in \"#{c}\"" unless m
28
+
29
+ {:file => m[1], :line => m[2].to_i, :method => m[4]}
30
+ end
31
+
32
+ def require resource, *modules_to_protect
33
+ new(*modules_to_protect) { TOPLEVEL_BINDING.eval("require '#{resource}'") }
34
+ end
35
+
36
+ def all_modules
37
+ unless @all_modules
38
+ @all_modules = [Object, Module, Class]
39
+ @all_modules << BasicObject if RUBY_VERSION >= '1.9'
40
+ # must run twice to get all modules correctly
41
+ all_modules
42
+ end
43
+ if RUBY_VERSION >= "1.9"
44
+ get_modules19 Object
45
+ else
46
+ get_modules18 Object
47
+ end
48
+ @all_modules.uniq!
49
+ @all_modules.dup
50
+ end
51
+
52
+ # set and get the default modules to protect, for the caller file.
53
+ def default_modules
54
+ (@default_modules ||= {})[caller_info[:file]] || []
55
+ end
56
+
57
+ def default_modules= modules_to_protect
58
+ (@default_modules ||= {})[caller_info[:file]] = [modules_to_protect].flatten
59
+ end
60
+
61
+ private
62
+
63
+ # get modules in use (differs from 1.8 to 1.9)
64
+ def get_modules18 base
65
+ new_mods = base.constants.map { |m| base.const_get(m) }.
66
+ select { |m| m.is_a?(Module) && !@all_modules.include?(m)}
67
+ unless new_mods.length == 0
68
+ @all_modules.push *new_mods
69
+ new_mods.each { |m| get_modules18(m) }
70
+ end
71
+ end
72
+
73
+ def get_modules19 base
74
+ new_mods = (base.constants - [:Config]).map { |m| base.const_get(m) }.
75
+ select { |m| m.is_a?(Module) && !@all_modules.include?(m)}
76
+ unless new_mods.length == 0
77
+ @all_modules.push *new_mods
78
+ new_mods.each { |m| get_modules19(m) }
79
+ end
80
+ end
81
+
82
+ end
83
+
84
+ # +modules+ are modules to protect.
85
+ # Must be the classes themselves, e.g., String, Symbol,
86
+ # not their names ("String" or :Symbol).
87
+ # Special names are:
88
+ # :all => protect all known modules and submodules. It's safer but slower.
89
+ # :core => protect the known top-level modules.
90
+ # :default => protect the modules defined in Namebox.default_modules for the caller file.
91
+ # Obs.: :core and :default can be used together and/or with other classes,
92
+ # but :all must be the only parameter if it is used.
93
+ def initialize *modules_to_protect, &blk
94
+
95
+ # initialize
96
+ @enabled_files = []
97
+ @protected_methods = []
98
+
99
+ # by default, get Namebox.default_modules
100
+ modules_to_protect = Namebox.default_modules if modules_to_protect.empty?
101
+ if modules_to_protect.include? :all
102
+ raise "If :all is used in Namebox.new, :all must be the only parameter!" if modules_to_protect.length > 1
103
+ @modules = Namebox.all_modules
104
+ else
105
+ @modules = modules_to_protect.select { |m| m.is_a? Module }
106
+ @modules += Namebox.default_modules if modules_to_protect.include? :default
107
+ @modules += CORE if modules_to_protect.include? :core
108
+ @modules.uniq!
109
+ end
110
+
111
+ raise "Modules to protect were not given and there's no Namebox.default_modules defined for file #{Namebox.caller_info[:file]}" if @modules.empty?
112
+
113
+ # update modules list and save preexisting data
114
+ methods_before = get_methods
115
+
116
+ blk.call
117
+
118
+ # compare with preexisting data to discover affected methods
119
+ methods_after = get_methods
120
+
121
+ # refine the new methods!
122
+ unless methods_after == methods_before
123
+
124
+ this_nb = self
125
+ methods_after.each do |fullname, info|
126
+
127
+ info_old = methods_before[fullname] || {}
128
+ new_m = info[:method]
129
+ old_m = info_old[:method]
130
+
131
+ # don't touch unmodified methods
132
+ next if new_m == old_m
133
+
134
+ # avoid reprotecting a method
135
+ owner = new_m.owner
136
+ protected_method = "#{owner}.#{info[:name]}"
137
+ next if @protected_methods.include? protected_method
138
+ @protected_methods << protected_method
139
+
140
+ if info[:type] == :instance
141
+ owner.send :define_method, info[:name] do |*args, &blk|
142
+ if this_nb.open?
143
+ new_m.bind(self).call(*args, &blk)
144
+ else
145
+ # try previous method or super
146
+ begin
147
+ old_m ? old_m.bind(self).call(*args, &blk) : super(*args, &blk)
148
+ rescue NoMethodError
149
+ obj = self.inspect.to_s
150
+ obj = obj[0..45] + '...' + obj[-1] if obj.length > 50
151
+ raise NoMethodError.new("Undefined instance method `#{info[:name]}' for #{obj}:#{self.class}")
152
+ end
153
+ end
154
+ end
155
+ else
156
+ # class methods
157
+ owner.send :define_method, info[:name] do |*args, &blk|
158
+ if this_nb.open?
159
+ new_m.call(*args, &blk)
160
+ else
161
+ # try previous method or super
162
+ begin
163
+ old_m ? old_m.call(*args, &blk) : super(*args, &blk)
164
+ rescue NoMethodError
165
+ raise NoMethodError.new("Undefined class method `#{info[:name]}' for #{self}:#{self.class}")
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
172
+
173
+ end
174
+
175
+ def open
176
+ info = ranges_info
177
+
178
+ # there must be no open range
179
+ raise "Namebox was already opened in #{info[:file]}:#{info[:last]}" if info[:last]
180
+
181
+ # range in progress
182
+ info[:ranges] << info[:line]
183
+ end
184
+
185
+ def close
186
+ info = ranges_info
187
+
188
+ # there must be an open range in progress
189
+ raise "Namebox was not opened in #{info[:file]} before line #{info[:line]}" unless info[:last]
190
+
191
+ # begin of range must be before end
192
+ r_beg = info[:last]
193
+ r_end = info[:line]
194
+ raise "Namebox#close in #{info[:file]}:#{r_end} should be after Namebox#open (line #{r_beg})" unless r_end >= r_beg
195
+ r = Range.new(r_beg, r_end)
196
+
197
+ # replace the single initial line with the range, making sure it's unique
198
+ info[:ranges].pop
199
+ info[:ranges] << r unless info[:ranges].include? r
200
+ end
201
+
202
+ def open?
203
+ # check file before checking ranges
204
+ ci = Namebox.caller_info
205
+ return true if @enabled_files.include? ci[:file]
206
+
207
+ # check ranges for this file
208
+ info = ranges_info ci
209
+ info[:ranges].each do |r|
210
+ case r
211
+ when Range
212
+ return true if r.include?(info[:line])
213
+ when Integer
214
+ return true if info[:line] >= r
215
+ end
216
+ end
217
+ false
218
+ end
219
+
220
+ def file_wide
221
+ @enabled_files << Namebox.caller_info[:file]
222
+ end
223
+
224
+ private
225
+
226
+ # Get line ranges info for the caller file
227
+ def ranges_info ci = Namebox.caller_info
228
+ ranges = enabled_ranges[ci[:file]] ||= []
229
+ ci[:ranges] = ranges
230
+
231
+ # check whether there is an opened range in progress for the caller file
232
+ last = ranges[-1]
233
+ ci[:last] = last if last.is_a? Integer
234
+
235
+ ci
236
+ end
237
+
238
+ # Stores enabled line ranges of caller files for this module
239
+ def enabled_ranges
240
+ @enabled_ranges ||= {}
241
+ end
242
+
243
+ # get methods in use
244
+ def get_methods
245
+ gm = {}
246
+ @modules.each do |c|
247
+ c.instance_methods.each do |m|
248
+ fullname = "#{c}##{m}"
249
+ gm[fullname] = {:type => :instance, :name => m, :method => c.instance_method(m) }
250
+ end
251
+ c.methods.each do |m|
252
+ fullname = "#{c}.#{m}"
253
+ gm[fullname] = {:type => :class, :name => m, :method => c.method(m) }
254
+ end
255
+ end
256
+ gm
257
+ end
258
+ end
metadata ADDED
@@ -0,0 +1,47 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: namebox
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Sony Santos
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-05 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: A simple hello world gem
15
+ email: sony.fermino@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/namebox.rb
21
+ homepage: http://rubychallenger.blogspot.com.br/2013/01/namebox.html
22
+ licenses:
23
+ - Public Domain
24
+ post_install_message:
25
+ rdoc_options: []
26
+ require_paths:
27
+ - lib
28
+ required_ruby_version: !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: 1.8.7
34
+ required_rubygems_version: !ruby/object:Gem::Requirement
35
+ none: false
36
+ requirements:
37
+ - - ! '>='
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ requirements: []
41
+ rubyforge_project:
42
+ rubygems_version: 1.8.24
43
+ signing_key:
44
+ specification_version: 3
45
+ summary: Create namespace boxes to protect the core classes' methods from changes,
46
+ like refinements
47
+ test_files: []