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