binding_dumper 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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