mochigome 0.1 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -58,8 +58,8 @@ gemspec = Gem::Specification.new do |s|
58
58
  s.authors = ["David Mike Simon"]
59
59
  s.email = "david.mike.simon@gmail.com"
60
60
  s.homepage = "http://github.com/DavidMikeSimon/mochigome"
61
- s.summary = "User-customizable report generator"
62
- s.description = "Report generator that uses your ActiveRecord associations"
61
+ s.summary = "The do-what-I-mean report generator"
62
+ s.description = "Report generator that graphs over ActiveRecord associations"
63
63
  s.files = `git ls-files .`.split("\n") - [".gitignore"]
64
64
  s.platform = Gem::Platform::RUBY
65
65
  s.require_path = 'lib'
@@ -69,6 +69,7 @@ gemspec = Gem::Specification.new do |s|
69
69
  s.add_dependency('ruport')
70
70
  s.add_dependency('nokogiri')
71
71
  s.add_dependency('rgl')
72
+ s.add_dependency('activerecord')
72
73
  end
73
74
 
74
75
  Gem::PackageTask.new(gemspec) do |pkg|
data/lib/mochigome_ver.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Mochigome
2
- VERSION = "0.1"
2
+ VERSION = "0.1.1"
3
3
  end
data/lib/model_graph.rb CHANGED
@@ -4,28 +4,30 @@ require 'rgl/traversal'
4
4
  module Mochigome
5
5
  private
6
6
 
7
- module ModelGraph
8
- @@graphed_models = Set.new
9
- @@table_to_model = {}
10
- @@assoc_graph = RGL::DirectedAdjacencyGraph.new
11
- @@edge_relation_funcs = {}
12
- @@shortest_paths = {}
7
+ class ModelGraph
8
+ def initialize
9
+ @graphed_models = Set.new
10
+ @table_to_model = {}
11
+ @assoc_graph = RGL::DirectedAdjacencyGraph.new
12
+ @edge_relation_funcs = {}
13
+ @shortest_paths = {}
14
+ end
13
15
 
14
16
  # Take an expression and return a Set of all models it references
15
- def self.expr_models(e)
17
+ def expr_models(e)
16
18
  r = Set.new
17
19
  [:expr, :left, :right].each do |m|
18
20
  r += expr_models(e.send(m)) if e.respond_to?(m)
19
21
  end
20
22
  if e.respond_to?(:relation)
21
- model = @@table_to_model[e.relation.name]
23
+ model = @table_to_model[e.relation.name]
22
24
  raise ModelSetupError.new("Table->model lookup error") unless model
23
25
  r.add model
24
26
  end
25
27
  r
26
28
  end
27
29
 
28
- def self.relation_over_path(path, rel = nil)
30
+ def relation_over_path(path, rel = nil)
29
31
  real_path = path.map{|e| (e.real_model? ? e : e.model)}.uniq
30
32
  # Project ensures that we return a Rel, not a Table, even if path is empty
31
33
  rel ||= real_path.first.arel_table.project
@@ -35,12 +37,12 @@ module Mochigome
35
37
  rel
36
38
  end
37
39
 
38
- def self.relation_func(u, v)
39
- @@edge_relation_funcs[[u,v]] or
40
+ def relation_func(u, v)
41
+ @edge_relation_funcs[[u,v]] or
40
42
  raise QueryError.new "No assoc from #{u.name} to #{v.name}"
41
43
  end
42
44
 
43
- def self.path_thru(models)
45
+ def path_thru(models)
44
46
  update_assoc_graph(models)
45
47
  model_queue = models.dup
46
48
  path = [model_queue.shift]
@@ -51,7 +53,7 @@ module Mochigome
51
53
  real_src = src.real_model? ? src : src.model
52
54
  real_tgt = tgt.real_model? ? tgt : tgt.model
53
55
  unless real_src == real_tgt
54
- seg = @@shortest_paths[[real_src,real_tgt]]
56
+ seg = @shortest_paths[[real_src,real_tgt]]
55
57
  unless seg
56
58
  raise QueryError.new("No path: #{real_src.name} to #{real_tgt.name}")
57
59
  end
@@ -68,20 +70,42 @@ module Mochigome
68
70
  path
69
71
  end
70
72
 
71
- def self.update_assoc_graph(models)
73
+ private
74
+
75
+ def update_assoc_graph(models)
72
76
  model_queue = models.dup
73
77
  added_models = []
74
78
  until model_queue.empty?
75
79
  model = model_queue.shift
76
80
  next if model.is_a?(SubgroupModel)
77
- next if @@graphed_models.include? model
78
- @@graphed_models.add model
81
+ next if @graphed_models.include? model
82
+ @graphed_models.add model
79
83
  added_models << model
80
84
 
81
- if @@table_to_model.has_key?(model.table_name)
82
- raise ModelError.new("Table #{model.table_name} used twice")
85
+ if @table_to_model.has_key?(model.table_name)
86
+ # TODO Test this!
87
+ # Find the nearest common ancestor that derives from AR::Base
88
+ common = nil
89
+ [model, @table_to_model[model.table_name]].each do |tgt|
90
+ a = tgt.ancestors
91
+ a = a.select{|c| c.ancestors.include?(ActiveRecord::Base)}
92
+ if common.nil?
93
+ common = a
94
+ else
95
+ common = common & a
96
+ end
97
+ end
98
+
99
+ if common.empty? || common.first == ActiveRecord::Base
100
+ raise ModelSetupError.new(
101
+ "Unrelated models %s and %s both claim to use table %s" %
102
+ [model, @table_to_model[model.table_name], model.table_name]
103
+ )
104
+ end
105
+ @table_to_model[model.table_name] = common.first
106
+ else
107
+ @table_to_model[model.table_name] = model
83
108
  end
84
- @@table_to_model[model.table_name] = model
85
109
 
86
110
  model.reflections.
87
111
  reject{|name, assoc| assoc.through_reflection}.
@@ -91,25 +115,25 @@ module Mochigome
91
115
  next if assoc.options[:polymorphic] # TODO How to deal with these? Check for matching has_X assoc?
92
116
  foreign_model = assoc.klass
93
117
  edge = [model, foreign_model]
94
- next if @@assoc_graph.has_edge?(*edge) # Ignore duplicate assocs
95
- @@assoc_graph.add_edge(*edge)
96
- @@edge_relation_funcs[edge] = model.arelified_assoc(name)
97
- unless @@graphed_models.include?(foreign_model)
118
+ next if @assoc_graph.has_edge?(*edge) # Ignore duplicate assocs
119
+ @assoc_graph.add_edge(*edge)
120
+ @edge_relation_funcs[edge] = model.arelified_assoc(name)
121
+ unless @graphed_models.include?(foreign_model)
98
122
  model_queue.push(foreign_model)
99
123
  end
100
124
  end
101
125
  end
102
126
 
103
127
  added_models.each do |model|
104
- next unless @@assoc_graph.has_vertex?(model)
105
- path_tree = @@assoc_graph.bfs_search_tree_from(model).reverse
128
+ next unless @assoc_graph.has_vertex?(model)
129
+ path_tree = @assoc_graph.bfs_search_tree_from(model).reverse
106
130
  path_tree.depth_first_search do |tgt_model|
107
131
  next if tgt_model == model
108
132
  path = [tgt_model]
109
133
  while (parent = path_tree.adjacent_vertices(path.first).first)
110
134
  path.unshift parent
111
135
  end
112
- @@shortest_paths[[model,tgt_model]] = path
136
+ @shortest_paths[[model,tgt_model]] = path
113
137
  end
114
138
 
115
139
  # Use through reflections as a hint for preferred indirect paths
@@ -123,8 +147,8 @@ module Mochigome
123
147
  # FIXME Can't handle polymorphic through reflection
124
148
  end
125
149
  edge = [model,foreign_model]
126
- next if @@shortest_paths[edge].try(:size).try(:<, 3)
127
- @@shortest_paths[edge] = [model, join_model, foreign_model]
150
+ next if @shortest_paths[edge].try(:size).try(:<, 3)
151
+ @shortest_paths[edge] = [model, join_model, foreign_model]
128
152
  end
129
153
  end
130
154
  end
data/lib/query.rb CHANGED
@@ -3,7 +3,6 @@ module Mochigome
3
3
  def initialize(layer_types, options = {})
4
4
  # TODO: Validate layer types: not empty, AR, act_as_mochigome_focus
5
5
  @layer_types = layer_types
6
- @layers_path = ModelGraph.path_thru(@layer_types)
7
6
 
8
7
  @name = options.delete(:root_name).try(:to_s) || "report"
9
8
  @access_filter = options.delete(:access_filter) || lambda {|cls| {}}
@@ -28,10 +27,8 @@ module Mochigome
28
27
  agg_rel.join_on_path_thru([focus_model, data_model])
29
28
  agg_rel.apply_access_filter_func(@access_filter)
30
29
 
31
- focus_idx = @layers_path.index(focus_model)
32
- key_path = focus_idx ? @layers_path.take(focus_idx+1) : @layers_path
33
- key_path = key_path.select{|m| @layer_types.include?(m)}
34
- key_cols = key_path.map{|m| m.arel_primary_key}
30
+ key_models = @ids_rel.spine_layers_thru(focus_model)
31
+ key_cols = key_models.map{|m| m.arel_primary_key}
35
32
 
36
33
  agg_fields = data_model.mochigome_aggregation_settings.
37
34
  options[:fields].reject{|a| a[:in_ruby]}
@@ -86,7 +83,7 @@ module Mochigome
86
83
  Mochigome Version: #{Mochigome::VERSION}
87
84
  Report Generated: #{Time.now}
88
85
  Layers: #{@layer_types.map(&:name).join(" => ")}
89
- AR Path: #{@layers_path.map(&:name).join(" => ")}
86
+ AR Path: #{@ids_rel.full_spine_path.map(&:name).join(" => ")}
90
87
  eos
91
88
  root.comment.gsub!(/(\n|^) +/, "\\1")
92
89
 
@@ -191,11 +188,12 @@ module Mochigome
191
188
 
192
189
  class Relation
193
190
  def initialize(layers)
191
+ @model_graph = ModelGraph.new
194
192
  @spine_layers = layers
195
- @spine = ModelGraph.path_thru(layers) or
193
+ @spine = @model_graph.path_thru(layers) or
196
194
  raise QueryError.new("No valid path thru #{layers.inspect}") #TODO Test
197
195
  @models = Set.new @spine
198
- @rel = ModelGraph.relation_over_path(@spine)
196
+ @rel = @model_graph.relation_over_path(@spine)
199
197
 
200
198
  @spine_layers.each{|m| select_model_id(m)}
201
199
  end
@@ -208,6 +206,16 @@ module Mochigome
208
206
  @rel.to_sql
209
207
  end
210
208
 
209
+ def full_spine_path
210
+ @spine.dup
211
+ end
212
+
213
+ def spine_layers_thru(model)
214
+ r = @spine.take_while{|m| m != model}
215
+ r << model unless r.size == @spine.size
216
+ r.select{|m| @spine_layers.include? m}
217
+ end
218
+
211
219
  def clone
212
220
  c = super
213
221
  c.instance_variable_set :@spine, @spine.dup
@@ -222,7 +230,7 @@ module Mochigome
222
230
  # Route to it in as few steps as possible, closer to spine end if tie.
223
231
  best_path = nil
224
232
  (@spine.reverse + (@models.to_a - @spine)).each do |link_model|
225
- path = ModelGraph.path_thru([link_model, model])
233
+ path = @model_graph.path_thru([link_model, model])
226
234
  if path && (best_path.nil? || path.size < best_path.size)
227
235
  best_path = path
228
236
  end
@@ -233,7 +241,7 @@ module Mochigome
233
241
  end
234
242
 
235
243
  def join_on_path_thru(path)
236
- join_on_path(ModelGraph.path_thru(path).uniq)
244
+ join_on_path(@model_graph.path_thru(path).uniq)
237
245
  end
238
246
 
239
247
  def join_on_path(path)
@@ -248,7 +256,7 @@ module Mochigome
248
256
  end
249
257
 
250
258
  def select_expr(e)
251
- ModelGraph.expr_models(e).each{|m| join_to_model(m)}
259
+ @model_graph.expr_models(e).each{|m| join_to_model(m)}
252
260
  @rel = @rel.project(e)
253
261
  end
254
262
 
@@ -264,7 +272,7 @@ module Mochigome
264
272
  end
265
273
  end
266
274
 
267
- ModelGraph.expr_models(cond).each{|m| join_to_model(m)}
275
+ @model_graph.expr_models(cond).each{|m| join_to_model(m)}
268
276
  @rel = @rel.where(cond)
269
277
  end
270
278
 
@@ -289,7 +297,7 @@ module Mochigome
289
297
  raise QueryError.new("Can't join from #{src}, not available") unless
290
298
  @models.include?(src)
291
299
  return if @models.include?(tgt) # TODO Maybe still apply join conditions?
292
- @rel = ModelGraph.relation_func(src, tgt).call(@rel)
300
+ @rel = @model_graph.relation_func(src, tgt).call(@rel)
293
301
  @models.add tgt
294
302
  end
295
303
  end
metadata CHANGED
@@ -1,12 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mochigome
3
3
  version: !ruby/object:Gem::Version
4
- hash: 9
4
+ hash: 25
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- version: "0.1"
9
+ - 1
10
+ version: 0.1.1
10
11
  platform: ruby
11
12
  authors:
12
13
  - David Mike Simon
@@ -14,7 +15,7 @@ autorequire:
14
15
  bindir: bin
15
16
  cert_chain: []
16
17
 
17
- date: 2012-04-09 00:00:00 Z
18
+ date: 2012-04-10 00:00:00 Z
18
19
  dependencies:
19
20
  - !ruby/object:Gem::Dependency
20
21
  version_requirements: &id001 !ruby/object:Gem::Requirement
@@ -73,7 +74,21 @@ dependencies:
73
74
  prerelease: false
74
75
  type: :runtime
75
76
  name: rgl
76
- description: Report generator that uses your ActiveRecord associations
77
+ - !ruby/object:Gem::Dependency
78
+ version_requirements: &id005 !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ hash: 3
84
+ segments:
85
+ - 0
86
+ version: "0"
87
+ requirement: *id005
88
+ prerelease: false
89
+ type: :runtime
90
+ name: activerecord
91
+ description: Report generator that graphs over ActiveRecord associations
77
92
  email: david.mike.simon@gmail.com
78
93
  executables: []
79
94
 
@@ -159,6 +174,6 @@ rubyforge_project: "[none]"
159
174
  rubygems_version: 1.8.6
160
175
  signing_key:
161
176
  specification_version: 3
162
- summary: User-customizable report generator
177
+ summary: The do-what-I-mean report generator
163
178
  test_files: []
164
179