binding_dumper 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +13 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +76 -0
- data/Rakefile +7 -0
- data/bin/console +15 -0
- data/bin/dummy_rails +3 -0
- data/bin/dummy_rake +4 -0
- data/bin/multitest +32 -0
- data/bin/setup +7 -0
- data/binding_dumper.gemspec +24 -0
- data/lib/binding_dumper.rb +28 -0
- data/lib/binding_dumper/core_ext/binding_ext.rb +67 -0
- data/lib/binding_dumper/core_ext/local_binding_patch_builder.rb +102 -0
- data/lib/binding_dumper/core_ext/magic_context_patch_builder.rb +55 -0
- data/lib/binding_dumper/dumpers/abstract.rb +73 -0
- data/lib/binding_dumper/dumpers/array_dumper.rb +64 -0
- data/lib/binding_dumper/dumpers/class_dumper.rb +111 -0
- data/lib/binding_dumper/dumpers/existing_object_dumper.rb +89 -0
- data/lib/binding_dumper/dumpers/hash_dumper.rb +69 -0
- data/lib/binding_dumper/dumpers/magic_dumper.rb +75 -0
- data/lib/binding_dumper/dumpers/object_dumper.rb +106 -0
- data/lib/binding_dumper/dumpers/primitive_dumper.rb +48 -0
- data/lib/binding_dumper/dumpers/proc_dumper.rb +44 -0
- data/lib/binding_dumper/magic_objects.rb +98 -0
- data/lib/binding_dumper/memories.rb +51 -0
- data/lib/binding_dumper/universal_dumper.rb +118 -0
- data/lib/binding_dumper/version.rb +3 -0
- data/test.rb +17 -0
- metadata +117 -0
@@ -0,0 +1,55 @@
|
|
1
|
+
# Class for buliding patch that adds method '_local_binding'
|
2
|
+
# to existing object
|
3
|
+
#
|
4
|
+
# @example
|
5
|
+
# data = {
|
6
|
+
# file: '/path/to/file.rb',
|
7
|
+
# line: 17,
|
8
|
+
# method: 'do_something',
|
9
|
+
# lvars: { a: 'b' }
|
10
|
+
# }
|
11
|
+
# patch = BindingDumper::CoreExt::MagicContextPatchBuilder.new(data).patch
|
12
|
+
# context = Object.new.extend(patch)
|
13
|
+
#
|
14
|
+
# context._local_binding
|
15
|
+
# # => #<Binding>
|
16
|
+
# context._local_binding.eval('a')
|
17
|
+
# # => 'b'
|
18
|
+
# context._local_binding.eval('__FILE__')
|
19
|
+
# # => '/path/to/file.rb'
|
20
|
+
# context._local_binding.eval('__LINE__')
|
21
|
+
# # => 17
|
22
|
+
# context._local_binding.eval('__method__')
|
23
|
+
# # => 'do_something'
|
24
|
+
#
|
25
|
+
module BindingDumper
|
26
|
+
module CoreExt
|
27
|
+
class MagicContextPatchBuilder
|
28
|
+
attr_reader :undumped
|
29
|
+
|
30
|
+
def initialize(undumped)
|
31
|
+
@undumped = undumped
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns module that is ready for patching existing context
|
35
|
+
#
|
36
|
+
# @return [Module]
|
37
|
+
#
|
38
|
+
def patch
|
39
|
+
undumped = self.undumped
|
40
|
+
Module.new do
|
41
|
+
define_method :_local_binding do
|
42
|
+
result = binding
|
43
|
+
|
44
|
+
undumped[:lvars].each do |lvar_name, lvar|
|
45
|
+
result.eval("#{lvar_name} = ObjectSpace._id2ref(#{lvar.object_id})")
|
46
|
+
end
|
47
|
+
|
48
|
+
mod = LocalBindingPatchBuilder.new(undumped).patch
|
49
|
+
result.extend(mod)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# Class with common functionality of all dumpers
|
2
|
+
#
|
3
|
+
class BindingDumper::Dumpers::Abstract
|
4
|
+
attr_reader :dumped_ids
|
5
|
+
|
6
|
+
# @param abstract_object [Object] any object
|
7
|
+
# @param dumped_ids [Array<Fixnum>] list of object ids that are already dumped
|
8
|
+
#
|
9
|
+
def initialize(abstract_object, dumped_ids = [])
|
10
|
+
@abstract_object = abstract_object
|
11
|
+
@dumped_ids = dumped_ids
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
# Returns abstract object
|
17
|
+
# Sometimes it's a Hash that represents object structure
|
18
|
+
# Sometimes it's just that object (if its' primitive)
|
19
|
+
#
|
20
|
+
# @return [Object]
|
21
|
+
#
|
22
|
+
def abstract_object
|
23
|
+
if @abstract_object.is_a?(Hash) && @abstract_object.has_key?(:_object_data)
|
24
|
+
@abstract_object[:_object_data]
|
25
|
+
else
|
26
|
+
@abstract_object
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns +true+ if +abstract_object+ should be converted
|
31
|
+
#
|
32
|
+
# @return [true, false]
|
33
|
+
#
|
34
|
+
def should_convert?
|
35
|
+
!dumped_ids.include?(abstract_object.object_id)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns true if +abstract_object+ can be dumped using Marshal.dump
|
39
|
+
#
|
40
|
+
# @return [true, false]
|
41
|
+
#
|
42
|
+
def can_be_fully_dumped?(object)
|
43
|
+
begin
|
44
|
+
Marshal.dump(object)
|
45
|
+
true
|
46
|
+
rescue TypeError, IOError
|
47
|
+
false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns +true+ if undumpable object (like StringIO.new)
|
52
|
+
# can't be dumped itself, but it's blank copy can be dumped
|
53
|
+
#
|
54
|
+
# @return [true, false]
|
55
|
+
#
|
56
|
+
def can_be_dumped_as_copy?(object)
|
57
|
+
begin
|
58
|
+
copy = object.class.allocate
|
59
|
+
Marshal.dump(copy)
|
60
|
+
true
|
61
|
+
rescue TypeError, IOError
|
62
|
+
false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns +true+ if object can't be marshaled
|
67
|
+
#
|
68
|
+
# @return [true, false]
|
69
|
+
#
|
70
|
+
def undumpable?(object)
|
71
|
+
!can_be_fully_dumped?(object) && !can_be_dumped_as_copy?(object)
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module BindingDumper
|
2
|
+
# Class responsible for converting arrays to marshalable Hash
|
3
|
+
#
|
4
|
+
# @example
|
5
|
+
# array = [1,2,3]
|
6
|
+
# dump = BindingDumper::Dumpers::Array.new(array).convert
|
7
|
+
# # => { marshalable: :hash }
|
8
|
+
# BindingDumper::Dumpers::Array.new(dump).deconvert
|
9
|
+
# # => [1,2,3]
|
10
|
+
#
|
11
|
+
class Dumpers::ArrayDumper < Dumpers::Abstract
|
12
|
+
alias_method :array, :abstract_object
|
13
|
+
|
14
|
+
# Returns true if ArrayDumper can convert +abstract_object+
|
15
|
+
#
|
16
|
+
# @return [true, false]
|
17
|
+
#
|
18
|
+
def can_convert?
|
19
|
+
array.is_a?(Array)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns true if ArrayDumper can deconvert +abstract_object+
|
23
|
+
#
|
24
|
+
# @return [true, false]
|
25
|
+
#
|
26
|
+
def can_deconvert?
|
27
|
+
array.is_a?(Array)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Converts +abstract_object+ to marshalable Hash
|
31
|
+
#
|
32
|
+
# @return [Hash]
|
33
|
+
#
|
34
|
+
def convert
|
35
|
+
unless should_convert?
|
36
|
+
return { _existing_object_id: array.object_id }
|
37
|
+
end
|
38
|
+
|
39
|
+
dumped_ids << array.object_id
|
40
|
+
|
41
|
+
result = array.map do |item|
|
42
|
+
UniversalDumper.convert(item, dumped_ids)
|
43
|
+
end
|
44
|
+
|
45
|
+
{
|
46
|
+
_old_object_id: array.object_id,
|
47
|
+
_object_data: result
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
# Deconverts passed +abstract_object+ back to the original state
|
52
|
+
#
|
53
|
+
# @return [Array]
|
54
|
+
#
|
55
|
+
def deconvert
|
56
|
+
result = []
|
57
|
+
yield result
|
58
|
+
array.each do |converted_item|
|
59
|
+
result << UniversalDumper.deconvert(converted_item)
|
60
|
+
end
|
61
|
+
result
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module BindingDumper
|
2
|
+
# Class responsible for converting classes to marshalable Hash
|
3
|
+
#
|
4
|
+
# @example
|
5
|
+
# class MyClass
|
6
|
+
# @a = 1
|
7
|
+
# @@b = 2
|
8
|
+
# end
|
9
|
+
# dump = BindingDumper::Dumpers::ClassDumper.new(MyClass).convert
|
10
|
+
# # => { marshalable: :hash }
|
11
|
+
# BindingDumper::Dumpers::ClassDumper.new(MyClass).deconvert
|
12
|
+
# # => MyClass
|
13
|
+
#
|
14
|
+
class Dumpers::ClassDumper < Dumpers::Abstract
|
15
|
+
alias_method :klass, :abstract_object
|
16
|
+
|
17
|
+
# Returns +true+ if ClassDumper can convert passed +abstract_object+
|
18
|
+
#
|
19
|
+
# @return [true, false]
|
20
|
+
#
|
21
|
+
def can_convert?
|
22
|
+
klass.is_a?(Class)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns +true+ if ClassDumper can deconvert passed +abstract_object+
|
26
|
+
#
|
27
|
+
# @return [true, false]
|
28
|
+
#
|
29
|
+
def can_deconvert?
|
30
|
+
abstract_object.is_a?(Hash) &&
|
31
|
+
(abstract_object.has_key?(:_cvars) || abstract_object.has_key?(:_anonymous))
|
32
|
+
end
|
33
|
+
|
34
|
+
# Converts passed +abstract_object+ to marshalable Hash
|
35
|
+
#
|
36
|
+
# @return [Hash]
|
37
|
+
#
|
38
|
+
def convert
|
39
|
+
return unless should_convert?
|
40
|
+
dumped_ids << klass.object_id
|
41
|
+
|
42
|
+
if klass.name
|
43
|
+
{
|
44
|
+
_klass: klass,
|
45
|
+
_ivars: converted_ivars(dumped_ids),
|
46
|
+
_cvars: converted_cvars(dumped_ids)
|
47
|
+
}
|
48
|
+
else
|
49
|
+
{
|
50
|
+
_anonymous: true
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
# Deconverts passed +abstract_object+ back to the original state
|
57
|
+
#
|
58
|
+
# @return [Class]
|
59
|
+
#
|
60
|
+
def deconvert
|
61
|
+
return Class.new if abstract_object[:_anonymous]
|
62
|
+
klass, converted_ivars, converted_cvars = abstract_object[:_klass], abstract_object[:_ivars], abstract_object[:_cvars]
|
63
|
+
|
64
|
+
converted_ivars.each do |ivar_name, converted_ivar|
|
65
|
+
ivar = UniversalDumper.deconvert(converted_ivar)
|
66
|
+
klass.instance_variable_set(ivar_name, ivar)
|
67
|
+
end
|
68
|
+
|
69
|
+
converted_cvars.each do |cvar_name, converted_cvar|
|
70
|
+
cvar = UniversalDumper.deconvert(converted_cvar)
|
71
|
+
klass.class_variable_set(cvar_name, cvar)
|
72
|
+
end
|
73
|
+
|
74
|
+
klass
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# Returns converted mapping of instance variables like
|
80
|
+
# { instance variable name => instance variable value }
|
81
|
+
#
|
82
|
+
# @return [Hash]
|
83
|
+
#
|
84
|
+
def converted_ivars(dumped_ids = [])
|
85
|
+
converted = klass.instance_variables.map do |ivar_name|
|
86
|
+
ivar = klass.instance_variable_get(ivar_name)
|
87
|
+
conveted_ivar = UniversalDumper.convert(ivar, dumped_ids)
|
88
|
+
[ivar_name, conveted_ivar]
|
89
|
+
end
|
90
|
+
Hash[converted]
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns converted mapping of class variables like
|
94
|
+
# { class variable name => class variable vakue }
|
95
|
+
#
|
96
|
+
# @return [Hash]
|
97
|
+
#
|
98
|
+
def converted_cvars(dumped_ids = [])
|
99
|
+
converted = klass.class_variables.map do |cvar_name|
|
100
|
+
ivar = klass.class_variable_get(cvar_name)
|
101
|
+
if dumped_ids.include?(ivar.object_id)
|
102
|
+
[]
|
103
|
+
else
|
104
|
+
conveted_ivar = UniversalDumper.convert(ivar, dumped_ids)
|
105
|
+
[cvar_name, conveted_ivar]
|
106
|
+
end
|
107
|
+
end
|
108
|
+
Hash[converted]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module BindingDumper
|
2
|
+
# Class responsible for restoring recurring objects
|
3
|
+
#
|
4
|
+
# If you dump an object twice you will get:
|
5
|
+
# First time: { marshalable: :hash }
|
6
|
+
# Second time: { _existing_object_id: 1234 }
|
7
|
+
#
|
8
|
+
# This _existing_object_id is actually an object_id of dumped object
|
9
|
+
# in original memory
|
10
|
+
#
|
11
|
+
# So, after deconverting the hash { _existing_object_id: ojbect_id }
|
12
|
+
# You will get an object from your current memory
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# class Profile
|
16
|
+
# attr_accessor :first_name, :last_name
|
17
|
+
# end
|
18
|
+
|
19
|
+
# profile = Profile.allocate
|
20
|
+
# profile.first_name = profile # <-- so the object is recursive
|
21
|
+
# profile.last_name = StringIO.new # <-- and unmarshalable
|
22
|
+
# dump = BindingDumper::UniversalDumper.convert(profile)
|
23
|
+
# =>
|
24
|
+
# {
|
25
|
+
# :_klass => Profile,
|
26
|
+
# :_ivars => {
|
27
|
+
# :@first_name => {
|
28
|
+
# :_existing_object_id => 47687640 # <-- right here
|
29
|
+
# },
|
30
|
+
# :@last_name => {
|
31
|
+
# :_klass => StringIO,
|
32
|
+
# :_undumpable => true
|
33
|
+
# }
|
34
|
+
# },
|
35
|
+
# :_old_object_id => 47687640
|
36
|
+
# }
|
37
|
+
#
|
38
|
+
# restored = BindingDumper::UniversalDumper.deconvert(profile)
|
39
|
+
# restored.profile.equal?(restored)
|
40
|
+
# # => true # (they have the same object id)
|
41
|
+
#
|
42
|
+
class Dumpers::ExistingObjectDumper < Dumpers::Abstract
|
43
|
+
alias_method :hash, :abstract_object
|
44
|
+
|
45
|
+
# Returns false, this class is only for deconverting
|
46
|
+
#
|
47
|
+
def can_convert?
|
48
|
+
false # really, it's only for deconverting
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns true if ExistingObjectDumper can deconvert passed +abstract_object+
|
52
|
+
#
|
53
|
+
# @return [true, false]
|
54
|
+
#
|
55
|
+
def can_deconvert?
|
56
|
+
hash.is_a?(Hash) && hash.has_key?(:_existing_object_id)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Raises an exception, don't use this class for converting
|
60
|
+
#
|
61
|
+
# @raise [NotImplementedError]
|
62
|
+
#
|
63
|
+
def convert
|
64
|
+
raise NotImplementedError
|
65
|
+
end
|
66
|
+
|
67
|
+
# Deconverts passed +abstract_object+ back to the original state
|
68
|
+
#
|
69
|
+
# @return [Object]
|
70
|
+
#
|
71
|
+
# @raise [RuntimeError] when object doesn't exist in the memory
|
72
|
+
#
|
73
|
+
def deconvert
|
74
|
+
data_without_object_id = hash.dup.delete(:_existing_object_id)
|
75
|
+
|
76
|
+
unless UniversalDumper.memories.has_key?(existing_object_id)
|
77
|
+
raise "Object with id #{existing_object_id} wasn't dumped. Something is wrong."
|
78
|
+
end
|
79
|
+
|
80
|
+
UniversalDumper.memories[existing_object_id]
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def existing_object_id
|
86
|
+
hash[:_existing_object_id]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module BindingDumper
|
2
|
+
# Class responsible for converting arbitary hashes to marshalable hashes
|
3
|
+
#
|
4
|
+
# @example
|
5
|
+
# hash = { key: 'value' }
|
6
|
+
# dump = BindingDumper::Dumpers::HashDumper.new(hash).convert
|
7
|
+
# BindingDumper::Dumpers::HashDumper.new(dump).deconvert
|
8
|
+
# # => { key: 'value' }
|
9
|
+
#
|
10
|
+
class Dumpers::HashDumper < Dumpers::Abstract
|
11
|
+
alias_method :hash, :abstract_object
|
12
|
+
|
13
|
+
# Returns true if HashDumper can convert passed +abstract_object+
|
14
|
+
#
|
15
|
+
# @return [true, false]
|
16
|
+
#
|
17
|
+
def can_convert?
|
18
|
+
hash.is_a?(Hash)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns true if HashDumper can deconvert passed +abstract_object+
|
22
|
+
#
|
23
|
+
# @return [true, false]
|
24
|
+
#
|
25
|
+
def can_deconvert?
|
26
|
+
abstract_object.is_a?(Hash)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Converts passed +abstract_object+ to marshalable Hash
|
30
|
+
#
|
31
|
+
# @return [Hash]
|
32
|
+
#
|
33
|
+
def convert
|
34
|
+
unless should_convert?
|
35
|
+
return { _existing_object_id: hash.object_id }
|
36
|
+
end
|
37
|
+
|
38
|
+
dumped_ids << hash.object_id
|
39
|
+
|
40
|
+
prepared = hash.map do |k, v|
|
41
|
+
converted_k = UniversalDumper.convert(k, dumped_ids)
|
42
|
+
converted_v = UniversalDumper.convert(v, dumped_ids)
|
43
|
+
[converted_k, converted_v]
|
44
|
+
end
|
45
|
+
|
46
|
+
result = Hash[prepared]
|
47
|
+
|
48
|
+
{
|
49
|
+
_old_object_id: hash.object_id,
|
50
|
+
_object_data: result
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
# Deconverts passed +abstract_object+ back to the original state
|
55
|
+
#
|
56
|
+
# @return [Hash]
|
57
|
+
#
|
58
|
+
def deconvert
|
59
|
+
result = {}
|
60
|
+
yield result
|
61
|
+
hash.each do |converted_k, converted_v|
|
62
|
+
k = UniversalDumper.deconvert(converted_k)
|
63
|
+
v = UniversalDumper.deconvert(converted_v)
|
64
|
+
result[k] = v
|
65
|
+
end
|
66
|
+
result
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|