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,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