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