modelist 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -1,3 +1,3 @@
1
1
  module Modelist
2
- VERSION = '0.0.3'
2
+ VERSION = '0.1.0'
3
3
  end
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.3
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: 2012-11-21 00:00:00.000000000 Z
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