modelist 0.0.3 → 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.
- data/README.md +36 -1
- data/lib/modelist/cli.rb +18 -0
- data/lib/modelist/path_finder.rb +194 -0
- data/lib/modelist/searcher.rb +43 -0
- data/lib/modelist/version.rb +1 -1
- metadata +4 -2
data/README.md
CHANGED
@@ -126,14 +126,49 @@ Example output:
|
|
126
126
|
|
127
127
|
Specify --output-file to provide an pathname of an errors file.
|
128
128
|
|
129
|
+
##### Search
|
130
|
+
|
131
|
+
Given a (partial) model/tablename/column/association name, finds all matching models/associations:
|
132
|
+
|
133
|
+
bundle exec modelist search partial_search_string
|
134
|
+
|
135
|
+
Example output:
|
136
|
+
|
137
|
+
Models:
|
138
|
+
Foo (table: foos)
|
139
|
+
Foobar (table: foobars)
|
140
|
+
Associations:
|
141
|
+
Bar (table: bars), association: foo (macro: belongs_to, options: {})
|
142
|
+
Loo (table: loos), association: f_users (macro: has_one, options: {:foreign_key=>:foo_id})
|
143
|
+
|
144
|
+
##### Path Finder
|
145
|
+
|
146
|
+
Given two model names will find known paths:
|
147
|
+
|
148
|
+
bundle exec modelist paths my_model_1 my_model_2
|
149
|
+
|
150
|
+
Example output:
|
151
|
+
|
152
|
+
checking for path from user to contact...
|
153
|
+
+++++++-+-++--+------
|
154
|
+
|
155
|
+
Paths from user to role (278):
|
156
|
+
|
157
|
+
user.role -> role
|
158
|
+
|
159
|
+
user.account -> account.role -> role
|
160
|
+
|
161
|
+
|
129
162
|
### API
|
130
163
|
|
131
164
|
Modelist::Analyst.find_required_models(:model1, :model2)
|
132
165
|
Modelist::CircularRefChecker.test_models(:model1, :model2, output_file: true)
|
133
166
|
Modelist::Tester.test_models(:model1, :model2, output_file: true)
|
167
|
+
Modelist::Searcher.find_all('foo')
|
168
|
+
Modelist::PathFinder.find_all(:model1, :model2)
|
134
169
|
|
135
170
|
### License
|
136
171
|
|
137
|
-
Copyright (c) 2012 Gary S. Weaver, released under the [MIT license][lic].
|
172
|
+
Copyright (c) 2012-2013 Gary S. Weaver, released under the [MIT license][lic].
|
138
173
|
|
139
174
|
[lic]: http://github.com/garysweaver/modelist/blob/master/LICENSE
|
data/lib/modelist/cli.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'thor'
|
2
|
+
require 'modelist/path_finder'
|
2
3
|
|
3
4
|
module Modelist
|
4
5
|
class CLI < Thor
|
@@ -31,6 +32,23 @@ module Modelist
|
|
31
32
|
args.each {|a| puts "Unsupported option: #{args.delete(a)}" if a.to_s.starts_with?('-')}
|
32
33
|
exit ::Modelist::CircularRefChecker.test_models(*args) ? 0 : 1
|
33
34
|
end
|
35
|
+
|
36
|
+
desc "search", "Given a (partial) model/tablename/column/association name, finds all matching models/associations."
|
37
|
+
def search(*args)
|
38
|
+
# load Rails environment
|
39
|
+
require './config/environment'
|
40
|
+
require 'modelist/searcher'
|
41
|
+
args.each {|a| puts "Unsupported option: #{args.delete(a)}" if a.to_s.starts_with?('-')}
|
42
|
+
exit ::Modelist::Searcher.find_all(*args) ? 0 : 1
|
43
|
+
end
|
44
|
+
|
45
|
+
desc "paths", "Given two model names will find known paths."
|
46
|
+
def paths(*args)
|
47
|
+
# load Rails environment
|
48
|
+
require './config/environment'
|
49
|
+
args.each {|a| puts "Unsupported option: #{args.delete(a)}" if a.to_s.starts_with?('-')}
|
50
|
+
exit ::Modelist::PathFinder.find_all(*args) ? 0 : 1
|
51
|
+
end
|
34
52
|
end
|
35
53
|
end
|
36
54
|
|
@@ -0,0 +1,194 @@
|
|
1
|
+
module Modelist
|
2
|
+
class PathFinder
|
3
|
+
DEFAULT_MAX_PATHS = 35
|
4
|
+
|
5
|
+
def self.clean_underscore(classname)
|
6
|
+
classname = classname[2..classname.length] if classname.start_with?('::')
|
7
|
+
classname.underscore
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.find_all(*args)
|
11
|
+
raise ArgumentError.new("Please supply a search term") unless args.size != 0
|
12
|
+
# less-dependent extract_options!
|
13
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
14
|
+
|
15
|
+
from = args[0]
|
16
|
+
to = args[1]
|
17
|
+
|
18
|
+
puts "Checking for path from #{from} to #{to}..."
|
19
|
+
relations = relationship_map_excluding(to)
|
20
|
+
|
21
|
+
results = get_all_paths_via_iterative_depth_first_search(from, to, relations)
|
22
|
+
puts
|
23
|
+
puts
|
24
|
+
|
25
|
+
matching_results = results.sort_by(&:length).reverse.collect{|arr|format_result_string(arr)}
|
26
|
+
#TODO: make it actually not even search past N nodes
|
27
|
+
if matching_results.length > 0
|
28
|
+
puts "Paths from #{from} to #{to} (#{matching_results.length}):"
|
29
|
+
# show the shortest path as the last item logged, for ease of use in CLI
|
30
|
+
matching_results.each do |r|
|
31
|
+
puts
|
32
|
+
puts r
|
33
|
+
end
|
34
|
+
else
|
35
|
+
puts "No path found from #{from} to #{to}."
|
36
|
+
end
|
37
|
+
results
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.relationship_map_excluding(exclude_name)
|
41
|
+
Rails.application.eager_load!
|
42
|
+
relations = {}
|
43
|
+
|
44
|
+
m_to_a = {}
|
45
|
+
ActiveRecord::Base.descendants.each do |m|
|
46
|
+
m_to_a[m] = m.reflect_on_all_associations
|
47
|
+
end
|
48
|
+
|
49
|
+
m_to_a.each do |m,as|
|
50
|
+
# don't try to link via composite primary keys until that is possible, which it isn't now afaik.
|
51
|
+
next if m.primary_key.is_a?(Array)
|
52
|
+
as.each do |association|
|
53
|
+
c1_name = clean_underscore(m.name)
|
54
|
+
next if c1_name == exclude_name
|
55
|
+
#TODO: we could exclude c1/key that is "to"
|
56
|
+
c1 = "#{c1_name}.#{association.name}"
|
57
|
+
c2 = get_classname(association)
|
58
|
+
if c2
|
59
|
+
relations[c1] = clean_underscore(c2)
|
60
|
+
end
|
61
|
+
end unless as == nil
|
62
|
+
end
|
63
|
+
relations
|
64
|
+
end
|
65
|
+
|
66
|
+
# e.g.
|
67
|
+
# from = 'model_name_1'
|
68
|
+
# to = 'model_name_2'
|
69
|
+
# directed_graph = {'model_name_3.assoc_name' => 'model_name_2', 'model_name_1.assoc_name' => 'model_name_3', 'model_name_2.assoc_name' => 'model_name_3', ...}
|
70
|
+
# returns: [['model_name_1.assoc_name', 'model_name_3.assoc_name', 'model_name_2'], ...]
|
71
|
+
def self.get_all_paths_via_iterative_depth_first_search(from, to, directed_graph)
|
72
|
+
queue = directed_graph.keys.select {|k| k.split('.')[0] == from && directed_graph[k] != from}
|
73
|
+
#puts "starting with #{queue.join(', ')}"
|
74
|
+
queue.each {|k| print '+'; $stdout.flush}
|
75
|
+
results = []
|
76
|
+
processed_result_partials = {} # model.assoc to array of results
|
77
|
+
current_node_list = []
|
78
|
+
class_assocs_visited = []
|
79
|
+
counts = [queue.length]
|
80
|
+
|
81
|
+
while queue.length > 0
|
82
|
+
#puts "queue(#{queue.length})=#{queue.inspect}"
|
83
|
+
#visualize_queue(queue)
|
84
|
+
this_class_assoc = queue.pop
|
85
|
+
class_assocs_visited << this_class_assoc unless class_assocs_visited.include?(this_class_assoc)
|
86
|
+
raise "FAILING! #{current_node_list[0].split('.')[0]} != #{from}" if current_node_list[0] && current_node_list[0].split('.')[0] != from
|
87
|
+
print '-'; $stdout.flush
|
88
|
+
next_class = directed_graph[this_class_assoc]
|
89
|
+
#puts "processing #{this_class_assoc} => #{next_class}"
|
90
|
+
current_node_list.push(this_class_assoc)
|
91
|
+
#puts "current_node_list(#{current_node_list.length})=#{current_node_list.inspect}"
|
92
|
+
#puts "counts: #{counts.join(',')}"
|
93
|
+
|
94
|
+
step_back = true
|
95
|
+
preprocessed_results = processed_result_partials[this_class_assoc]
|
96
|
+
if preprocessed_results
|
97
|
+
#puts "already processed #{this_class_assoc}"
|
98
|
+
print '-'; $stdout.flush
|
99
|
+
if preprocessed_results.length > 0
|
100
|
+
results << (current_node_list + preprocessed_results)
|
101
|
+
#raise "bug in preprocessed! should start with #{from} but have result #{format_result_string(results.last)}" if current_node_list[0].split('.')[0] != from
|
102
|
+
end
|
103
|
+
elsif next_class == to
|
104
|
+
#puts "reached #{to} in #{current_node_list.length} steps"
|
105
|
+
found_path = current_node_list + [next_class]
|
106
|
+
results << found_path
|
107
|
+
raise "oops! should start with #{from} but have result #{format_result_string(results.last)}" if current_node_list[0].split('.')[0] != from
|
108
|
+
cache_found_path_partials(found_path, processed_result_partials)
|
109
|
+
elsif !current_node_list.any?{|n| n.start_with?("#{next_class}.")}
|
110
|
+
children_to_visit = directed_graph.select {|k,v| k.start_with?("#{next_class}.") && directed_graph[k] != from}.keys
|
111
|
+
#puts "following (#{children_to_visit.length}): #{children_to_visit.join(', ')}"
|
112
|
+
children_to_visit.each {|c|print '+'; $stdout.flush}
|
113
|
+
if children_to_visit.length > 0
|
114
|
+
step_back = false
|
115
|
+
counts.push(children_to_visit.length)
|
116
|
+
queue += children_to_visit
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
back(current_node_list, counts, processed_result_partials, from) if step_back
|
121
|
+
end
|
122
|
+
|
123
|
+
#puts
|
124
|
+
#puts
|
125
|
+
#unvisited = directed_graph.keys - class_assocs_visited
|
126
|
+
#puts "unvisited associations (#{unvisited.length}): #{unvisited.sort.join(', ')}"
|
127
|
+
#puts
|
128
|
+
|
129
|
+
results
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.forward(current_node_list, this_class_assoc, counts, children_count)
|
133
|
+
current_node_list.push(this_class_assoc)
|
134
|
+
counts.push(children_count)
|
135
|
+
end
|
136
|
+
|
137
|
+
def self.back(current_node_list, counts, processed_result_partials, from)
|
138
|
+
current_node_list.pop
|
139
|
+
|
140
|
+
# if there is a count in the array less than two, keep removing them until they are gone
|
141
|
+
while counts.last && counts.last < 2
|
142
|
+
counts.pop
|
143
|
+
c = current_node_list.pop
|
144
|
+
|
145
|
+
# if this is direct/indirect circular reference, don't mark unprocessed origin as processed, because it isn't
|
146
|
+
unless counts.size > 1 && c.start_with?("#{from}.")
|
147
|
+
# completely processed this path, so don't process it again, and don't overwrite existing success if cached
|
148
|
+
#puts "marking #{c} as done"
|
149
|
+
processed_result_partials[c] = [] unless processed_result_partials[c]
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# either there are no counts, or we just need to decrement the last count
|
154
|
+
if counts.last && counts.last > 1
|
155
|
+
counts[counts.length-1] = counts[counts.length-1] - 1
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def self.cache_found_path_partials(found_path_arr, cache)
|
160
|
+
(found_path_arr.length-2).downto(0) do |i|
|
161
|
+
cache[found_path_arr[i]] = found_path_arr[i+1..found_path_arr.length-1] if found_path_arr.length > 1
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def self.format_result_string(*args)
|
166
|
+
args.flatten.join(' -> ')
|
167
|
+
end
|
168
|
+
|
169
|
+
def self.get_classname(association)
|
170
|
+
association.options[:class_name] || case association.macro
|
171
|
+
when :belongs_to, :has_one
|
172
|
+
association.name.to_s
|
173
|
+
when :has_and_belongs_to_many, :has_many
|
174
|
+
association.name.to_s.singularize
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
#def self.visualize_queue(queue)
|
179
|
+
# puts
|
180
|
+
# puts "QUEUE:"
|
181
|
+
# puts
|
182
|
+
# last_root = queue[0].split('.')[0]
|
183
|
+
# queue.each do |item|
|
184
|
+
# if last_root.to_sym != item.split('.')[0].to_sym
|
185
|
+
# puts " |"
|
186
|
+
# end
|
187
|
+
# puts item
|
188
|
+
# last_root = item.split('.')[0].to_sym
|
189
|
+
# end
|
190
|
+
# puts
|
191
|
+
# puts "-------------"
|
192
|
+
#end
|
193
|
+
end
|
194
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Modelist
|
2
|
+
class Searcher
|
3
|
+
|
4
|
+
# Given a (partial) model/tablename/column/association name, finds all matching models/associations, e.g.
|
5
|
+
# Modelist::Searcher.find_all('bar')
|
6
|
+
# would return the following if there were a model named Foobar, a table named moobars, a column named
|
7
|
+
# barfoo_id in the users table, and an association named foobars on the Loo model:
|
8
|
+
# Models:
|
9
|
+
# Foobar (table: examples)
|
10
|
+
# Moo (table: moobars)
|
11
|
+
# Associations:
|
12
|
+
# User.barfoo (foreign key: barfoo_id)
|
13
|
+
# Loo.foobars
|
14
|
+
def self.find_all(*args)
|
15
|
+
# less-dependent extract_options!
|
16
|
+
#options = args.last.is_a?(Hash) ? args.pop : {}
|
17
|
+
raise ArgumentError.new("Please supply a search term") unless args.size != 0
|
18
|
+
Rails.application.eager_load!
|
19
|
+
models = []
|
20
|
+
associations = []
|
21
|
+
search_term = args[0].downcase
|
22
|
+
ActiveRecord::Base.descendants.each do |m|
|
23
|
+
if m.name.to_s.downcase[search_term] || m.name.to_s.underscore[search_term] || m.table_name.to_s.downcase[search_term] || m.table_name.to_s.underscore[search_term]
|
24
|
+
val = "#{m.name} (table: #{m.table_name})"
|
25
|
+
models << val unless models.include?(val)
|
26
|
+
end
|
27
|
+
m.reflect_on_all_associations.each do |a|
|
28
|
+
if a.name.to_s.downcase[search_term] || a.name.to_s.underscore[search_term] || a.options.values.any?{|v| v.to_s.downcase[search_term] || v.to_s.underscore[search_term]}
|
29
|
+
val = "#{m.name} (table: #{m.table_name}), association: #{a.name} (macro: #{a.macro.inspect}, options: #{a.options.inspect})"
|
30
|
+
associations << val unless models.include?(val)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
puts "Models:"
|
36
|
+
models.each {|a| puts " #{a}"}
|
37
|
+
puts "Associations:"
|
38
|
+
associations.each {|a| puts " #{a}"}
|
39
|
+
|
40
|
+
models.length > 0 || association.length > 0
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/modelist/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: modelist
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-06-27 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: thor
|
@@ -57,6 +57,8 @@ files:
|
|
57
57
|
- lib/modelist/circular_ref_checker.rb
|
58
58
|
- lib/modelist/cli.rb
|
59
59
|
- lib/modelist/config.rb
|
60
|
+
- lib/modelist/path_finder.rb
|
61
|
+
- lib/modelist/searcher.rb
|
60
62
|
- lib/modelist/tester.rb
|
61
63
|
- lib/modelist/version.rb
|
62
64
|
- lib/modelist.rb
|