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,75 @@
|
|
1
|
+
module BindingDumper
|
2
|
+
# Class responsible for converting 'magical' objects to marshalable hash
|
3
|
+
#
|
4
|
+
# You should define which objects are 'magical'.
|
5
|
+
# The difference between 'magical' and 'regular' objects is that
|
6
|
+
# 'magical' objects are the same for every sessions
|
7
|
+
# like Rails.application.config or Rails.env
|
8
|
+
#
|
9
|
+
# To make object 'magical', use BindingDumper::MagicObjects.register(object)
|
10
|
+
#
|
11
|
+
# When you convert a 'magical' object to marshalable hash it returns 'the way how to get it',
|
12
|
+
# not the object itself
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# class A
|
16
|
+
# class << self
|
17
|
+
# @config = :config
|
18
|
+
# attr_reader :config
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
# BindingDumper::MagicObjects.register(A)
|
22
|
+
# BindingDumper::MagicObjects.pool
|
23
|
+
# {
|
24
|
+
# 47472500 => "A",
|
25
|
+
# 600668 => "A.instance_variable_get(:@config)"
|
26
|
+
# }
|
27
|
+
# # (the mapping 'object id' => 'the way to get an object')
|
28
|
+
#
|
29
|
+
# dump = BindingDumper::Dumpers::MagicDumper.new(A.config).convert
|
30
|
+
# # => { :_magic => "A.instance_variable_get(:@config)" }
|
31
|
+
# restored = BindingDumper::Dumpers::MagicDumper.new(dump).deconvert
|
32
|
+
# # => :config
|
33
|
+
#
|
34
|
+
class Dumpers::MagicDumper < Dumpers::Abstract
|
35
|
+
alias_method :object, :abstract_object
|
36
|
+
|
37
|
+
# Returns true if MagicDumper can convert passed +abstract_object+
|
38
|
+
#
|
39
|
+
# @return [true, false]
|
40
|
+
#
|
41
|
+
def can_convert?
|
42
|
+
MagicObjects.magic?(object)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns true if MagicDumper can deconvert passed +abstract_object+
|
46
|
+
#
|
47
|
+
# @return [true, false]
|
48
|
+
#
|
49
|
+
def can_deconvert?
|
50
|
+
abstract_object.is_a?(Hash) && abstract_object.has_key?(:_magic)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Converts passed +abstract_object+ to marshalable Hash
|
54
|
+
#
|
55
|
+
# @return [Hash]
|
56
|
+
#
|
57
|
+
def convert
|
58
|
+
return unless should_convert?
|
59
|
+
|
60
|
+
object_magic = MagicObjects.get_magic(object)
|
61
|
+
{
|
62
|
+
_magic: object_magic
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
# Deconverts passed +abstract_object+ back to the original state
|
67
|
+
#
|
68
|
+
# @return [Object]
|
69
|
+
#
|
70
|
+
def deconvert
|
71
|
+
# release magic!
|
72
|
+
eval(abstract_object[:_magic])
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module BindingDumper
|
2
|
+
# Class responsible for converting objects to marshalable Hash
|
3
|
+
#
|
4
|
+
# @example
|
5
|
+
# o = Object.new
|
6
|
+
# dump = BindingDumper::Dumpers::Array.new(o).convert
|
7
|
+
# # => { marshalable: :hash }
|
8
|
+
# BindingDumper::Dumpers::Array.new(dump).deconvert
|
9
|
+
# # => o
|
10
|
+
#
|
11
|
+
class Dumpers::ObjectDumper < Dumpers::Abstract
|
12
|
+
alias_method :object, :abstract_object
|
13
|
+
|
14
|
+
# Returns true if ObjectDumper can convert passed +abstract_object+
|
15
|
+
#
|
16
|
+
# @return [true, false]
|
17
|
+
#
|
18
|
+
def can_convert?
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns true if ObjectDumper can deconvert passed +abstract_object+
|
23
|
+
#
|
24
|
+
# @return [true, false]
|
25
|
+
#
|
26
|
+
def can_deconvert?
|
27
|
+
abstract_object.is_a?(Hash) &&
|
28
|
+
(
|
29
|
+
abstract_object.has_key?(:_klass) ||
|
30
|
+
abstract_object.has_key?(:_object) ||
|
31
|
+
abstract_object.has_key?(:_old_object_id)
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Converts passed +abstract_object+ to marshalable Hash
|
36
|
+
#
|
37
|
+
# @return [Hash]
|
38
|
+
#
|
39
|
+
def convert
|
40
|
+
unless should_convert?
|
41
|
+
return { _existing_object_id: object.object_id }
|
42
|
+
end
|
43
|
+
|
44
|
+
if can_be_fully_dumped?(object)
|
45
|
+
{
|
46
|
+
_object: object
|
47
|
+
}
|
48
|
+
elsif undumpable?(object)
|
49
|
+
{
|
50
|
+
_klass: object.class,
|
51
|
+
_undumpable: true
|
52
|
+
}
|
53
|
+
else
|
54
|
+
dumped_ids << object.object_id
|
55
|
+
{
|
56
|
+
_klass: object.class,
|
57
|
+
_ivars: converted_ivars(dumped_ids),
|
58
|
+
_old_object_id: object.object_id
|
59
|
+
}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Deconverts passed +abstract_object+ back to the original state
|
64
|
+
#
|
65
|
+
# @return [Object]
|
66
|
+
#
|
67
|
+
def deconvert
|
68
|
+
if object.has_key?(:_object)
|
69
|
+
object[:_object]
|
70
|
+
else
|
71
|
+
klass = object[:_klass]
|
72
|
+
result = klass.allocate
|
73
|
+
return result if object[:_undumpable]
|
74
|
+
|
75
|
+
yield result
|
76
|
+
|
77
|
+
object[:_ivars].each do |ivar_name, converted_ivar|
|
78
|
+
ivar = UniversalDumper.deconvert(converted_ivar)
|
79
|
+
result.instance_variable_set(ivar_name, ivar)
|
80
|
+
end
|
81
|
+
|
82
|
+
if result.respond_to?(:restored_from_binding)
|
83
|
+
result.restored_from_binding
|
84
|
+
end
|
85
|
+
|
86
|
+
result
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# Returns converted mapping of instance variables like
|
93
|
+
# { instance variable name => instance variable value }
|
94
|
+
#
|
95
|
+
# @return [Hash]
|
96
|
+
#
|
97
|
+
def converted_ivars(dumped_ids = [])
|
98
|
+
converted = object.instance_variables.map do |ivar_name|
|
99
|
+
ivar = object.instance_variable_get(ivar_name)
|
100
|
+
conveted_ivar = UniversalDumper.convert(ivar, dumped_ids)
|
101
|
+
[ivar_name, conveted_ivar]
|
102
|
+
end.reject(&:empty?)
|
103
|
+
Hash[converted]
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module BindingDumper
|
2
|
+
# Class responsible for converting primitive objects to marshalable hash
|
3
|
+
#
|
4
|
+
# @see SUPPORTED_CLASSES
|
5
|
+
#
|
6
|
+
class Dumpers::PrimitiveDumper < Dumpers::Abstract
|
7
|
+
alias_method :primitive, :abstract_object
|
8
|
+
|
9
|
+
SUPPORTED_CLASSES = [
|
10
|
+
Numeric,
|
11
|
+
String,
|
12
|
+
NilClass,
|
13
|
+
FalseClass,
|
14
|
+
TrueClass,
|
15
|
+
Symbol
|
16
|
+
]
|
17
|
+
|
18
|
+
# Returns true if PrimitiveDumper can convert passed +abstract_object+
|
19
|
+
#
|
20
|
+
# @return [true, false]
|
21
|
+
#
|
22
|
+
def can_convert?
|
23
|
+
SUPPORTED_CLASSES.any? do |klass|
|
24
|
+
abstract_object.is_a?(klass)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def can_deconvert?
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns +abstract_object+
|
33
|
+
#
|
34
|
+
# @return [Object]
|
35
|
+
#
|
36
|
+
def convert
|
37
|
+
primitive
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns +abstract_object+
|
41
|
+
#
|
42
|
+
# @return [Object]
|
43
|
+
#
|
44
|
+
def deconvert
|
45
|
+
primitive
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module BindingDumper
|
2
|
+
# Class responsible for converting procs and methods to marshalable hash
|
3
|
+
#
|
4
|
+
# It uses a gem called 'method_source' which may inspect the source of proc/method
|
5
|
+
#
|
6
|
+
class Dumpers::ProcDumper < Dumpers::Abstract
|
7
|
+
alias_method :_proc, :abstract_object
|
8
|
+
|
9
|
+
# Returns true if ProcDumper can convert passed +abstract_object+
|
10
|
+
#
|
11
|
+
# @return [true, false]
|
12
|
+
#
|
13
|
+
def can_convert?
|
14
|
+
_proc.is_a?(Proc) || _proc.is_a?(Method)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns true if ProcDumper can deconvert pased +abstract_object+
|
18
|
+
#
|
19
|
+
# @return [true, false]
|
20
|
+
#
|
21
|
+
def can_deconvert?
|
22
|
+
abstract_object.is_a?(Hash) && abstract_object.has_key?(:_source)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Converts passed +abstract_object+ to marshalable hash
|
26
|
+
#
|
27
|
+
# @return [Hash]
|
28
|
+
#
|
29
|
+
def convert
|
30
|
+
return unless should_convert?
|
31
|
+
|
32
|
+
source = (_proc.to_proc.source rescue 'proc {}').strip
|
33
|
+
{ _source: source }
|
34
|
+
end
|
35
|
+
|
36
|
+
# Deconverts passed +abstract_object+ back to the original state
|
37
|
+
#
|
38
|
+
# @return [Object]
|
39
|
+
#
|
40
|
+
def deconvert
|
41
|
+
eval(_proc[:_source]) rescue proc {}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# Module responsible for storing and retrieving 'magical' objects
|
2
|
+
# Object is 'magical' if it's the same for every Ruby process
|
3
|
+
#
|
4
|
+
# Examples of 'magical' objects:
|
5
|
+
# Rails.application
|
6
|
+
# Rails.env
|
7
|
+
# Rails.application.config
|
8
|
+
#
|
9
|
+
# To register an object, run:
|
10
|
+
# @example
|
11
|
+
# BindingDumper::MagicObjects.register(MyClass)
|
12
|
+
# # or if it's method that returns always the same data
|
13
|
+
# BindingDumper::MagicObjects.register(:some_method, 'send(:some_method)')
|
14
|
+
#
|
15
|
+
# After marking an object as 'magical' it (and all embedded objects)
|
16
|
+
# will be added to the object pool of 'magical' objects
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
# class A
|
20
|
+
# class << self
|
21
|
+
# attr_reader :config
|
22
|
+
# @config = { config: :data }
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# BindingDumper::MagicObjects.register(A)
|
27
|
+
# BindingDumper::MagicObjects.pool
|
28
|
+
# {
|
29
|
+
# 47472500 => "A",
|
30
|
+
# 600668 => "A.instance_variable_get(:@config)"
|
31
|
+
# }
|
32
|
+
# BindingDumper::MagicObjects.magic?(A.config)
|
33
|
+
# # => true
|
34
|
+
# BindingDumper::MagicObjects.get_magic(A.config)
|
35
|
+
# # => "A.instance_variable_get(:@config)"
|
36
|
+
#
|
37
|
+
module BindingDumper::MagicObjects
|
38
|
+
# Builds a tree of objects inside of passed +object+
|
39
|
+
#
|
40
|
+
# @return [Hash]
|
41
|
+
#
|
42
|
+
def self.magic_tree_from(object, object_path, result = {})
|
43
|
+
return if result[object.object_id]
|
44
|
+
|
45
|
+
result[object.object_id] = object_path
|
46
|
+
|
47
|
+
object.instance_variables.each do |ivar_name|
|
48
|
+
path = "#{object_path}.instance_variable_get(:#{ivar_name})"
|
49
|
+
ivar = object.instance_variable_get(ivar_name)
|
50
|
+
magic_tree_from(ivar, path, result)
|
51
|
+
end
|
52
|
+
|
53
|
+
result
|
54
|
+
end
|
55
|
+
|
56
|
+
# Registers passed object as 'magical'
|
57
|
+
#
|
58
|
+
# @param object [Object]
|
59
|
+
# @param object_path [String] the way how to get an object
|
60
|
+
#
|
61
|
+
def self.register(object, object_path = object.name)
|
62
|
+
tree = magic_tree_from(object, object_path)
|
63
|
+
pool.merge!(tree)
|
64
|
+
true
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns Hash containing all magical objects
|
68
|
+
#
|
69
|
+
# @return [Hash] in format { object_id => way to get an object }
|
70
|
+
#
|
71
|
+
def self.pool
|
72
|
+
@pool ||= {}
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns true if passed +object+ is 'magical'
|
76
|
+
#
|
77
|
+
# @return [true, false]
|
78
|
+
#
|
79
|
+
def self.magic?(object)
|
80
|
+
pool.has_key?(object.object_id)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns the way to get a 'magical' object
|
84
|
+
#
|
85
|
+
# @param object [Object]
|
86
|
+
#
|
87
|
+
# @return [String]
|
88
|
+
#
|
89
|
+
def self.get_magic(object)
|
90
|
+
pool[object.object_id]
|
91
|
+
end
|
92
|
+
|
93
|
+
# Flushes existing information about 'magical' objects
|
94
|
+
#
|
95
|
+
def self.flush!
|
96
|
+
@pool = {}
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# Module for storing mapping { dumped object_id => restored object }
|
2
|
+
# used for BindingDumper::Dumpers::ExistingObjectDumper strategy
|
3
|
+
#
|
4
|
+
# @see BindingDumper::UniversalDumper
|
5
|
+
# @example
|
6
|
+
# BindingDumper::UniversalDumper.memories
|
7
|
+
# # => {}
|
8
|
+
# o = Object.new
|
9
|
+
# BindingDumper::UniversalDumper.remember!(123, o)
|
10
|
+
# BindingDumper::UniversalDumper.with_memories(123) == o
|
11
|
+
# # => true
|
12
|
+
#
|
13
|
+
module BindingDumper::Memories
|
14
|
+
# Returns hash containing all restored objects
|
15
|
+
#
|
16
|
+
# @return [Hash]
|
17
|
+
#
|
18
|
+
def memories
|
19
|
+
@memories ||= {}
|
20
|
+
end
|
21
|
+
|
22
|
+
# Flushes existing memories about restored objects
|
23
|
+
#
|
24
|
+
def flush_memories!
|
25
|
+
@memories = {}
|
26
|
+
end
|
27
|
+
|
28
|
+
# Saves passed +object+ and marks it with +object_id+
|
29
|
+
#
|
30
|
+
# @param object [Object]
|
31
|
+
# @param object_id [Fixnum]
|
32
|
+
#
|
33
|
+
def remember!(object, object_id)
|
34
|
+
memories[object_id] = object
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns an object from memories with +old_object_id+
|
38
|
+
# or yields
|
39
|
+
#
|
40
|
+
# @param old_object_id [Fixnum]
|
41
|
+
#
|
42
|
+
# @yield
|
43
|
+
#
|
44
|
+
def with_memories(old_object_id, &block)
|
45
|
+
if memories.has_key?(old_object_id)
|
46
|
+
memories[old_object_id]
|
47
|
+
else
|
48
|
+
yield
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module BindingDumper
|
2
|
+
# Module that puts together all existing dumpers and wraps their functionality
|
3
|
+
#
|
4
|
+
# This dumper can dump and load back _any_ object using system of existing dumpers.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# dump = BindingDumper::UniversalDumper.dump(Object.new)
|
8
|
+
# restored = BindingDumper::UniversalDumper.load(dump)
|
9
|
+
# # => #<Object>
|
10
|
+
#
|
11
|
+
module UniversalDumper
|
12
|
+
DUMPERS_ON_CONVERTING = [
|
13
|
+
Dumpers::MagicDumper,
|
14
|
+
Dumpers::ProcDumper,
|
15
|
+
Dumpers::ClassDumper,
|
16
|
+
Dumpers::ArrayDumper,
|
17
|
+
Dumpers::HashDumper,
|
18
|
+
Dumpers::PrimitiveDumper,
|
19
|
+
Dumpers::ObjectDumper
|
20
|
+
]
|
21
|
+
|
22
|
+
DUMPERS_ON_DECONVERTING = [
|
23
|
+
Dumpers::MagicDumper,
|
24
|
+
Dumpers::ExistingObjectDumper,
|
25
|
+
Dumpers::ProcDumper,
|
26
|
+
Dumpers::ArrayDumper,
|
27
|
+
Dumpers::ClassDumper,
|
28
|
+
Dumpers::ObjectDumper,
|
29
|
+
Dumpers::HashDumper,
|
30
|
+
Dumpers::PrimitiveDumper
|
31
|
+
]
|
32
|
+
|
33
|
+
extend self
|
34
|
+
extend BindingDumper::Memories
|
35
|
+
|
36
|
+
# Returns converter that should be applied to provided +object+
|
37
|
+
#
|
38
|
+
# @param object [Object]
|
39
|
+
# @param dumped_ids [Array<Fixum>] list of object_ids that are already dumped
|
40
|
+
#
|
41
|
+
# @return [BindingDumper::Dumpers::Abstract]
|
42
|
+
#
|
43
|
+
def converter_for(object, dumped_ids)
|
44
|
+
DUMPERS_ON_CONVERTING.map { |dumper_klass|
|
45
|
+
dumper_klass.new(object, dumped_ids)
|
46
|
+
}.detect(&:can_convert?)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Converts passed +object+ to marshalable string
|
50
|
+
#
|
51
|
+
# @param object [Object]
|
52
|
+
# @param dumped_ids [Array<Fixnum>] list of object_ids that are already dumped
|
53
|
+
#
|
54
|
+
# @return [Hash]
|
55
|
+
#
|
56
|
+
def convert(object, dumped_ids = [])
|
57
|
+
converter_for(object, dumped_ids).convert
|
58
|
+
end
|
59
|
+
|
60
|
+
# Dump passed +object+ to string
|
61
|
+
#
|
62
|
+
# @param object [Object]
|
63
|
+
#
|
64
|
+
# @return [String]
|
65
|
+
#
|
66
|
+
def dump(object)
|
67
|
+
converted = convert(object)
|
68
|
+
Marshal.dump(converted)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns deconverter that should be applied to provided +object+
|
72
|
+
#
|
73
|
+
# @param object [Object]
|
74
|
+
#
|
75
|
+
# @return [BindingDumper::Dumpers::Abstract]
|
76
|
+
#
|
77
|
+
def deconverter_for(object)
|
78
|
+
DUMPERS_ON_DECONVERTING.map do |dumper_klass|
|
79
|
+
dumper_klass.new(object)
|
80
|
+
end.detect do |dumper|
|
81
|
+
dumper.can_deconvert?
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Deconverts passed +object+ to marshalable string
|
86
|
+
#
|
87
|
+
# @param object [Object]
|
88
|
+
#
|
89
|
+
# @return [Object]
|
90
|
+
#
|
91
|
+
def deconvert(converted_data)
|
92
|
+
deconverter = deconverter_for(converted_data)
|
93
|
+
|
94
|
+
if deconverter.is_a?(Dumpers::PrimitiveDumper)
|
95
|
+
return deconverter.deconvert
|
96
|
+
end
|
97
|
+
|
98
|
+
old_object_id = converted_data[:_old_object_id]
|
99
|
+
|
100
|
+
with_memories(old_object_id) do
|
101
|
+
deconverter.deconvert do |object|
|
102
|
+
remember!(object, old_object_id)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Loads dumped object back to its original state
|
108
|
+
#
|
109
|
+
# @param object [String]
|
110
|
+
#
|
111
|
+
# @return [Object]
|
112
|
+
#
|
113
|
+
def load(object)
|
114
|
+
converted = Marshal.load(object)
|
115
|
+
deconvert(converted)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|