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.
@@ -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