active_explorer 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,233 @@
1
+ require 'writer'
2
+ require 'painter'
3
+ require 'config'
4
+
5
+ module ActiveExplorer
6
+ class Exploration
7
+ ASSOCIATION_FILTER_VALUES = [:has_many, :has_one, :belongs_to, :all]
8
+
9
+ # Creates new exploration and generates exploration hash.
10
+ #
11
+ # @param association_filter [Array]
12
+ # Values of array: `:has_many`, `:has_one`, `:belongs_to`, `:all`.
13
+ # When empty then it "follows previous association" (i.e. uses `:belongs_to` when previous assoc. was `:belongs_to` and
14
+ # uses `:has_xxx` when previous assoc. was `:has_xxx`). To always follow all associations you must specify
15
+ # all associations (e.g. uses `ActiveExplorer::Exploration::ASSOCIATION_FILTER_VALUES` as a value).
16
+ #
17
+ # @param class_filter [Array or Hash]
18
+ # If Array is used then it means to show only those classes in Array.
19
+ # When Hash is used then it can have these keys:
20
+ # - `:show` - Shows these classes, ignores at all other classes.
21
+ # - `:ignore` - Stops processing at these, does not show it and does not go to children. Processing goes back to parent.
22
+ # Use plural form (e.g. `books`).
23
+ #
24
+ # @param depth [Integer]
25
+ # How deep into the subobjects should the explorere go. Depth 1 is only direct children. Depth 0 returns no children.
26
+ #
27
+ def initialize(object, depth: 5, class_filter: nil, attribute_filter: nil, attribute_limit: nil, association_filter: nil, parent_object: nil)
28
+ raise TypeError, "Parameter 'class_filter' must be Array or Hash but is #{class_filter.class}." unless class_filter.nil? || class_filter.is_a?(Array) || class_filter.is_a?(Hash)
29
+ raise TypeError, "Parameter 'association_filter' must be Array but is #{association_filter.class}." unless association_filter.nil? || association_filter.is_a?(Array)
30
+ raise TypeError, "Parameter 'association_filter' must only contain values #{ASSOCIATION_FILTER_VALUES.to_s[1..-2]}." unless association_filter.nil? || association_filter.empty? || (association_filter & ASSOCIATION_FILTER_VALUES).any?
31
+
32
+ @object = object
33
+ @depth = depth
34
+ @parent_object = parent_object
35
+
36
+ @attribute_limit = attribute_limit || ActiveExplorer::Config.attribute_limit
37
+ @attribute_filter = attribute_filter || ActiveExplorer::Config.attribute_filter
38
+
39
+ @hash = { class_name: make_safe(@object.class.name),
40
+ attributes: attributes }
41
+
42
+ unless @depth.zero?
43
+ @class_filter = class_filter || ActiveExplorer::Config.class_filter
44
+ @class_filter = { show: @class_filter } if @class_filter.is_a?(Array)
45
+
46
+ [:show, :ignore].each do |group|
47
+ @class_filter[group] = @class_filter[group].present? ? each_val_to_s(@class_filter[group]) : []
48
+ end
49
+
50
+ @association_filter = association_filter || ActiveExplorer::Config.association_filter
51
+ @association_filter = ASSOCIATION_FILTER_VALUES if @association_filter.present? && @association_filter.include?(:all)
52
+
53
+ @associations = associtations(@object, @class_filter, @association_filter)
54
+
55
+ subobject_hash = subobjects_hash(@object, @associations)
56
+ @hash[:subobjects] = subobject_hash unless subobject_hash.empty?
57
+ end
58
+ end
59
+
60
+ def attributes
61
+ attrs = @object.attributes.symbolize_keys
62
+ attrs = attrs.first(@attribute_limit).to_h if @attribute_limit
63
+
64
+ return attrs if @attribute_filter.nil?
65
+
66
+ filter = @attribute_filter[@object.class.name.downcase.pluralize.to_sym]
67
+
68
+ if filter
69
+ attrs.select { |key| filter.include?(key) }
70
+ else
71
+ attrs
72
+ end
73
+ end
74
+
75
+ def get_hash
76
+ @hash.deep_dup
77
+ end
78
+
79
+ def to_console
80
+ Writer.new(self).write
81
+ end
82
+
83
+ def to_image(file, origin_as_root: false)
84
+ Painter.new(self, file).paint(origin_as_root: origin_as_root)
85
+ end
86
+
87
+ private
88
+
89
+ def subobjects_hash(object, associations)
90
+ associations.each_with_object([]) do |association, results|
91
+ case association_type(association)
92
+ when :belongs_to
93
+ subobject = subobjects_from_association(object, association)
94
+
95
+ if subobject.present?
96
+ results.push subobject_hash(association, object, subobject) unless is_parent?(subobject)
97
+ end
98
+
99
+ when :has_many, :has_one
100
+ subobjects = subobjects_from_association(object, association)
101
+
102
+ if subobjects.present?
103
+ subobjects.each do |subobject|
104
+ results.push subobject_hash(association, object, subobject) unless is_parent?(subobject)
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ def subobject_hash(association, object, subobject)
112
+ association_type = association_type(association)
113
+
114
+ exploration = explore(subobject, parent_object: object, association_type: association_type)
115
+
116
+ hash = exploration.get_hash
117
+ hash[:association] = association_type
118
+ hash
119
+ end
120
+
121
+ def subobjects_from_association(object, association)
122
+ subobjects = object.send(association.name)
123
+ subobjects.present? ? subobjects : nil
124
+
125
+ rescue NameError, ActiveRecord::StatementInvalid, ActiveRecord::RecordNotFound => e
126
+ association_type = is_has_many_association?(association) ? 'has_many' : 'belongs_to'
127
+ add_error_hash("#{e.message} in #{association_type} :#{association.name}")
128
+ nil
129
+ end
130
+
131
+ def explore(object, parent_object:, association_type:)
132
+ association_filter = if !@association_filter.nil? && @association_filter.any?
133
+ @association_filter
134
+ elsif association_type == :belongs_to
135
+ [:belongs_to]
136
+ else
137
+ [:has_many, :has_one]
138
+ end
139
+
140
+ Exploration.new object,
141
+ depth: @depth - 1,
142
+ class_filter: @class_filter,
143
+ attribute_filter: @attribute_filter,
144
+ attribute_limit: @attribute_limit,
145
+ association_filter: association_filter,
146
+ parent_object: parent_object
147
+ end
148
+
149
+ def associtations(object, class_filter, association_filter)
150
+ associations = object.class.reflections.collect do |reflection|
151
+ reflection.second
152
+ end
153
+
154
+ if !association_filter.nil? && association_filter.any? && !association_filter.include?(:all)
155
+ associations.select! do |association|
156
+ association_filter.include? association_type(association)
157
+ end
158
+ end
159
+
160
+ if class_filter
161
+ if class_filter[:show].any?
162
+ associations.select! do |association|
163
+ should_show?(association)
164
+ end
165
+ elsif class_filter[:ignore].any?
166
+ associations.reject! do |association|
167
+ should_ignore?(association)
168
+ end
169
+ end
170
+ end
171
+
172
+ associations
173
+ end
174
+
175
+ def add_error_hash(message)
176
+ @hash[:class_name] = make_safe(@object.class.name)
177
+ @hash[:attributes] = attributes
178
+
179
+ @hash[:error_message] = "Error in #{@object.class.name}(#{@object.id}): #{message}"
180
+ end
181
+
182
+ def association_type(association)
183
+ if association.is_a?(ActiveRecord::Reflection::HasManyReflection) ||
184
+ association.is_a?(ActiveRecord::Reflection::ThroughReflection) ||
185
+ association.is_a?(ActiveRecord::Reflection::HasAndBelongsToManyReflection)
186
+ :has_many
187
+ elsif association.is_a?(ActiveRecord::Reflection::HasOneReflection)
188
+ :has_one
189
+ elsif association.is_a?(ActiveRecord::Reflection::BelongsToReflection)
190
+ :belongs_to
191
+ end
192
+ end
193
+
194
+ def make_short(text)
195
+ text.length < 70 ? text : text[0..70] + " (...)"
196
+ end
197
+
198
+ # Replace characters that conflict with DOT language (used in GraphViz).
199
+ # These: `{`, `}`, `<`, `>`, `|`, `\`
200
+ #
201
+ def make_safe(text)
202
+ text.tr('{}<>|\\', '')
203
+ end
204
+
205
+ def each_val_to_s(array)
206
+ array.collect { |a| a.to_s }
207
+ end
208
+
209
+ def is_belongs_to_association?(association)
210
+ association.is_a?(ActiveRecord::Reflection::BelongsToReflection)
211
+ end
212
+
213
+ def is_has_many_association?(association)
214
+ association.is_a?(ActiveRecord::Reflection::HasManyReflection)
215
+ end
216
+
217
+ def is_has_one_association?(association)
218
+ association.is_a?(ActiveRecord::Reflection::HasOneReflection)
219
+ end
220
+
221
+ def is_parent?(object)
222
+ object === @parent_object
223
+ end
224
+
225
+ def should_show?(association)
226
+ @class_filter[:show].include? association.plural_name.to_s
227
+ end
228
+
229
+ def should_ignore?(association)
230
+ @class_filter[:ignore].include? association.plural_name.to_s
231
+ end
232
+ end
233
+ end
data/lib/painter.rb ADDED
@@ -0,0 +1,101 @@
1
+ module ActiveExplorer
2
+ class Painter
3
+ def initialize(exploration, file_path)
4
+ @exploration = exploration
5
+ @file_path = file_path
6
+ @graph = GraphViz.new(:G, :type => :digraph)
7
+ end
8
+
9
+ def paint(origin_as_root: false)
10
+ @centralized = origin_as_root
11
+ paint_object @exploration.get_hash, @graph, nil
12
+ save_to_file
13
+ @graph
14
+ end
15
+
16
+ private
17
+
18
+ def paint_object(hash, graph, parent_node)
19
+ style = parent_node.nil? ? :origin : nil
20
+
21
+ node = add_node(hash, graph, style: style)
22
+ add_edge(graph, parent_node, node, hash[:association]) unless parent_node.nil?
23
+
24
+ paint_subobjects graph, node, hash[:subobjects] unless hash[:subobjects].nil?
25
+ end
26
+
27
+ def paint_subobjects(graph, parent_node, subhashes)
28
+ subhashes.each do |hash|
29
+ paint_object hash, graph, parent_node
30
+ end
31
+ end
32
+
33
+ def add_node(hash, graph, style: nil)
34
+ id = hash[:attributes][:id]
35
+ class_name = make_safe(hash[:class_name])
36
+ attributes = make_safe(hash[:attributes].keys.join("\n"))
37
+ values = hash[:attributes].values.collect do |val|
38
+ if val.nil?
39
+ 'nil'
40
+ elsif val.is_a? String
41
+ "\"#{make_short(val)}\""
42
+ else
43
+ make_short(val.to_s)
44
+ end
45
+ end
46
+ values = make_safe(values.join("\n"))
47
+
48
+ if style == :origin
49
+ graph.add_node("#{class_name}_#{id}", shape: "record", label: "{#{class_name}|{#{attributes}|#{values}}}", labelloc: 't', style: 'filled', fillcolor: 'yellow')
50
+ else
51
+ graph.add_node("#{class_name}_#{id}", shape: "record", label: "{#{class_name}|{#{attributes}|#{values}}}", labelloc: 't')
52
+ end
53
+ end
54
+
55
+ def add_edge(graph, parent_node, node, association)
56
+ if @centralized
57
+ graph.add_edge(parent_node, node, label: association == :belongs_to ? ' belongs to' : ' has') unless edge_exists?(graph, parent_node, node)
58
+ else
59
+ if association == :belongs_to
60
+ graph.add_edge(node, parent_node) unless edge_exists?(graph, node, parent_node)
61
+ else
62
+ graph.add_edge(parent_node, node) unless edge_exists?(graph, parent_node, node)
63
+ end
64
+ end
65
+ end
66
+
67
+ def edge_exists?(graph, node_one, node_two)
68
+ graph.each_edge do |edge|
69
+ return true if edge.node_one == node_one.id && edge.node_two == node_two.id
70
+ end
71
+
72
+ false
73
+ end
74
+
75
+ def save_to_file
76
+ filename = @file_path.split(File::SEPARATOR).last
77
+ directory = @file_path.chomp filename
78
+
79
+ create_directory directory unless directory.empty?
80
+
81
+ @graph.output(:png => @file_path)
82
+ end
83
+
84
+ def create_directory(directory)
85
+ unless directory.empty? || File.directory?(directory)
86
+ FileUtils.mkdir_p directory
87
+ end
88
+ end
89
+
90
+ def make_short(text)
91
+ text.length < 70 ? text : text[0..70] + " (...)"
92
+ end
93
+
94
+ # Replace characters that conflict with DOT language (used in GraphViz).
95
+ # These: `{`, `}`, `<`, `>`, `|`, `\`
96
+ #
97
+ def make_safe(text)
98
+ text.tr('{}<>|\\', '')
99
+ end
100
+ end
101
+ end
data/lib/writer.rb ADDED
@@ -0,0 +1,48 @@
1
+ module ActiveExplorer
2
+ class Writer
3
+ def initialize(exploration)
4
+ @exploration = exploration
5
+ end
6
+
7
+ def write
8
+ exploration_hash = @exploration.get_hash
9
+
10
+ puts "\nExplored #{exploration_hash[:class_name]}(#{exploration_hash[:attributes][:id]}):\n\n"
11
+ write_object(exploration_hash)
12
+ puts "\n"
13
+ end
14
+
15
+ private
16
+
17
+ def write_object(object, level = 0)
18
+ class_name = object[:class_name]
19
+ id = object[:attributes][:id]
20
+ attributes = object[:attributes]
21
+ error_message = object[:error_message]
22
+
23
+ attributes.delete :id
24
+
25
+ margin = ' ' * level
26
+
27
+ if level > 0
28
+ margin[-2] = '->'
29
+ margin += object[:association] == :belongs_to ? 'belongs to ' : 'has '
30
+ end
31
+
32
+ puts "#{margin}#{class_name}(#{id}) #{attributes}"
33
+
34
+ if error_message.present?
35
+ margin = ' ' * level
36
+ puts "#{margin}(#{error_message})" if error_message.present?
37
+ end
38
+
39
+ write_objects object[:subobjects], level + 1 unless object[:subobjects].nil?
40
+ end
41
+
42
+ def write_objects(objects, level)
43
+ objects.each do |object|
44
+ write_object object, level
45
+ end
46
+ end
47
+ end
48
+ end
metadata ADDED
@@ -0,0 +1,284 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_explorer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.7
5
+ platform: ruby
6
+ authors:
7
+ - Marek Ulicny
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-06-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ruby-graphviz
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 4.2.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 4.2.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.10'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.10'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 3.0.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 3.0.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec-nc
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: guard
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: guard-rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: ruby-graphviz
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '1.2'
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: 1.2.2
135
+ type: :development
136
+ prerelease: false
137
+ version_requirements: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - "~>"
140
+ - !ruby/object:Gem::Version
141
+ version: '1.2'
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: 1.2.2
145
+ - !ruby/object:Gem::Dependency
146
+ name: mysql
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ type: :development
153
+ prerelease: false
154
+ version_requirements: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ - !ruby/object:Gem::Dependency
160
+ name: standalone_migrations
161
+ requirement: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ type: :development
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ - !ruby/object:Gem::Dependency
174
+ name: awesome_print
175
+ requirement: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
180
+ type: :development
181
+ prerelease: false
182
+ version_requirements: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ - !ruby/object:Gem::Dependency
188
+ name: factory_girl
189
+ requirement: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - ">="
192
+ - !ruby/object:Gem::Version
193
+ version: '0'
194
+ type: :development
195
+ prerelease: false
196
+ version_requirements: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: '0'
201
+ - !ruby/object:Gem::Dependency
202
+ name: appraisal
203
+ requirement: !ruby/object:Gem::Requirement
204
+ requirements:
205
+ - - ">="
206
+ - !ruby/object:Gem::Version
207
+ version: '0'
208
+ type: :development
209
+ prerelease: false
210
+ version_requirements: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - ">="
213
+ - !ruby/object:Gem::Version
214
+ version: '0'
215
+ description: Visualization of data and associations represented by Active Record.
216
+ email:
217
+ - xulicny@gmail.com
218
+ executables:
219
+ - console
220
+ - setup
221
+ extensions: []
222
+ extra_rdoc_files: []
223
+ files:
224
+ - ".gitignore"
225
+ - ".rspec"
226
+ - ".ruby-gemset"
227
+ - ".ruby-version"
228
+ - ".standalone_migrations"
229
+ - ".travis.yml"
230
+ - Appraisals
231
+ - CODE_OF_CONDUCT.md
232
+ - Gemfile
233
+ - Guardfile
234
+ - LICENSE.txt
235
+ - README.md
236
+ - Rakefile
237
+ - active_explorer.gemspec
238
+ - bin/console
239
+ - bin/setup
240
+ - gemfiles/activerecord_3_0.gemfile
241
+ - gemfiles/activerecord_3_0.gemfile.lock
242
+ - gemfiles/activerecord_3_1.gemfile
243
+ - gemfiles/activerecord_3_1.gemfile.lock
244
+ - gemfiles/activerecord_3_2.gemfile
245
+ - gemfiles/activerecord_3_2.gemfile.lock
246
+ - gemfiles/activerecord_4_0.gemfile
247
+ - gemfiles/activerecord_4_0.gemfile.lock
248
+ - gemfiles/activerecord_4_1.gemfile
249
+ - gemfiles/activerecord_4_1.gemfile.lock
250
+ - gemfiles/activerecord_4_2.gemfile
251
+ - gemfiles/activerecord_4_2.gemfile.lock
252
+ - gemfiles/activerecord_5_0.gemfile
253
+ - gemfiles/activerecord_5_0.gemfile.lock
254
+ - lib/active_explorer.rb
255
+ - lib/active_explorer/version.rb
256
+ - lib/config.rb
257
+ - lib/exploration.rb
258
+ - lib/painter.rb
259
+ - lib/writer.rb
260
+ homepage: https://github.com/rascasone/active_explorer
261
+ licenses:
262
+ - MIT
263
+ metadata: {}
264
+ post_install_message:
265
+ rdoc_options: []
266
+ require_paths:
267
+ - lib
268
+ required_ruby_version: !ruby/object:Gem::Requirement
269
+ requirements:
270
+ - - ">="
271
+ - !ruby/object:Gem::Version
272
+ version: '0'
273
+ required_rubygems_version: !ruby/object:Gem::Requirement
274
+ requirements:
275
+ - - ">="
276
+ - !ruby/object:Gem::Version
277
+ version: '0'
278
+ requirements: []
279
+ rubyforge_project:
280
+ rubygems_version: 2.5.1
281
+ signing_key:
282
+ specification_version: 4
283
+ summary: Visualization of data and associations represented by Active Record.
284
+ test_files: []