data_graph 1.0.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/History +3 -0
- data/License.txt +22 -0
- data/README +48 -0
- data/lib/data_graph.rb +9 -0
- data/lib/data_graph/cpk_linkage.rb +21 -0
- data/lib/data_graph/graph.rb +92 -0
- data/lib/data_graph/linkage.rb +127 -0
- data/lib/data_graph/node.rb +290 -0
- data/lib/data_graph/utils.rb +59 -0
- data/lib/data_graph/version.rb +7 -0
- metadata +120 -0
data/History
ADDED
data/License.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2010 Pinnacol Assurance
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software. Except as contained in this
|
12
|
+
notice, the name(s) of the above copyright holders shall not be used in
|
13
|
+
advertising or otherwise to promote the sale, use or other dealings in this
|
14
|
+
Software without prior written authorization.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
22
|
+
SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
= DataGraph
|
2
|
+
|
3
|
+
Simplified eager loading for ActiveRecord
|
4
|
+
|
5
|
+
== Description
|
6
|
+
|
7
|
+
The default eager loading mechanism of ActiveRecord has numerous cases where
|
8
|
+
these two are not equivalent as you might expect:
|
9
|
+
|
10
|
+
Model.find(:first, :include => :assoc).assoc
|
11
|
+
Model.find(:first).assoc
|
12
|
+
|
13
|
+
As a result it gets tricky to make associations that work correctly via
|
14
|
+
include. Oftentimes too much data gets returned. DataGraph makes eager loading
|
15
|
+
easier by providing a way to declare and load a specific set of associated
|
16
|
+
data.
|
17
|
+
|
18
|
+
== Usage
|
19
|
+
|
20
|
+
DataGraph uses a syntax based on the serialization methods.
|
21
|
+
|
22
|
+
require 'data_graph'
|
23
|
+
graph = Model.data_graph(
|
24
|
+
:only => [:a, :b, :c],
|
25
|
+
:include => {
|
26
|
+
:assoc => {
|
27
|
+
:only => [:x, :y]
|
28
|
+
}})
|
29
|
+
|
30
|
+
data = graph.find(:first)
|
31
|
+
data.a # => 'A'
|
32
|
+
data.assoc.x # => 'X'
|
33
|
+
data.assoc.z # !> ActiveRecord::MissingAttributeError
|
34
|
+
|
35
|
+
Any number of associations may be specified this way, and to any nesting
|
36
|
+
depth. DataGraph always uses a 'one query per-association' strategy and never
|
37
|
+
reverts to left outer joins the way include sometimes will.
|
38
|
+
|
39
|
+
== Installation
|
40
|
+
|
41
|
+
DataGraph is available as a gem on {Gemcutter}[http://gemcutter.org/gems/data_graph]
|
42
|
+
|
43
|
+
% gem install data_graph
|
44
|
+
|
45
|
+
== Info
|
46
|
+
|
47
|
+
Developer:: {Simon Chiang}[http://bahuvrihi.wordpress.com]
|
48
|
+
License:: {MIT-Style}[link:files/License_txt.html]
|
data/lib/data_graph.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'data_graph/linkage'
|
2
|
+
|
3
|
+
module DataGraph
|
4
|
+
class CpkLinkage < Linkage
|
5
|
+
def parent_id(record)
|
6
|
+
parent_columns.collect {|attribute| record.send(attribute) }
|
7
|
+
end
|
8
|
+
|
9
|
+
def child_id(record)
|
10
|
+
child_columns.collect {|attribute| record.send(attribute) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def conditions(id_map)
|
14
|
+
condition = child_columns.collect {|col| "#{table_name}.#{connection.quote_column_name(col)} = ?" }.join(' AND ')
|
15
|
+
conditions = Array.new(id_map.length, condition)
|
16
|
+
conditions_str = "(#{conditions.join(') OR (')})"
|
17
|
+
|
18
|
+
id_map.keys.flatten.unshift(conditions_str)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'data_graph/node'
|
2
|
+
|
3
|
+
module DataGraph
|
4
|
+
class Graph
|
5
|
+
include Utils
|
6
|
+
|
7
|
+
attr_reader :model
|
8
|
+
attr_reader :aliases
|
9
|
+
attr_reader :paths
|
10
|
+
attr_reader :subsets
|
11
|
+
attr_reader :node
|
12
|
+
|
13
|
+
def initialize(node, options={})
|
14
|
+
aliases = options[:aliases]
|
15
|
+
subsets = options[:subsets]
|
16
|
+
|
17
|
+
@node = node
|
18
|
+
@nest_paths = node.nest_paths
|
19
|
+
@aliases = @node.aliases
|
20
|
+
@aliases.merge!(aliases) if aliases
|
21
|
+
|
22
|
+
@paths = {}
|
23
|
+
@subsets = {}
|
24
|
+
|
25
|
+
subsets = {
|
26
|
+
:default => '*',
|
27
|
+
:get => node.get_paths,
|
28
|
+
:set => node.set_paths
|
29
|
+
}.merge(subsets || {})
|
30
|
+
|
31
|
+
subsets.each_pair do |name, unresolved_paths|
|
32
|
+
resolved_paths = resolve(unresolved_paths)
|
33
|
+
@paths[name] = resolved_paths
|
34
|
+
@subsets[name] = node.only(resolved_paths)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def find(*args)
|
39
|
+
node.find(*args)
|
40
|
+
end
|
41
|
+
|
42
|
+
def paginate(*args)
|
43
|
+
node.paginate(*args)
|
44
|
+
end
|
45
|
+
|
46
|
+
def only(paths)
|
47
|
+
node.only validate(:get, resolve(paths))
|
48
|
+
end
|
49
|
+
|
50
|
+
def except(paths)
|
51
|
+
node.only validate(:get, resolve(paths))
|
52
|
+
end
|
53
|
+
|
54
|
+
def resolve(paths)
|
55
|
+
paths = paths.collect {|path| aliases[path.to_s] || path }
|
56
|
+
paths.flatten!
|
57
|
+
paths.collect! {|path| path.to_s }
|
58
|
+
paths.uniq!
|
59
|
+
paths
|
60
|
+
end
|
61
|
+
|
62
|
+
def path(type)
|
63
|
+
paths[type] or raise "no such path: #{type.inspect}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def subset(type, default_type = :default)
|
67
|
+
(subsets[type] || subsets[default_type]) or raise "no such subset: #{type.inspect}"
|
68
|
+
end
|
69
|
+
|
70
|
+
def validate(type, paths)
|
71
|
+
inaccessible_paths = paths - path(type)
|
72
|
+
unless inaccessible_paths.empty?
|
73
|
+
raise InaccessiblePathError.new(inaccessible_paths)
|
74
|
+
end
|
75
|
+
paths
|
76
|
+
end
|
77
|
+
|
78
|
+
def validate_attrs(type, attrs)
|
79
|
+
validate(type, patherize_attrs(attrs, @nest_paths))
|
80
|
+
attrs
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class InaccessiblePathError < RuntimeError
|
85
|
+
attr_reader :paths
|
86
|
+
|
87
|
+
def initialize(paths)
|
88
|
+
@paths = paths
|
89
|
+
super "inaccesible: #{paths.inspect}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'data_graph/utils'
|
2
|
+
|
3
|
+
module DataGraph
|
4
|
+
class Linkage
|
5
|
+
include Utils
|
6
|
+
|
7
|
+
attr_reader :macro
|
8
|
+
attr_reader :name
|
9
|
+
attr_reader :through
|
10
|
+
attr_reader :table_name
|
11
|
+
attr_reader :connection
|
12
|
+
attr_reader :parent_columns
|
13
|
+
attr_reader :child_columns
|
14
|
+
attr_reader :child_node
|
15
|
+
|
16
|
+
def initialize(assoc, options={})
|
17
|
+
@macro = assoc.macro
|
18
|
+
@name = assoc.name
|
19
|
+
@through = nil
|
20
|
+
|
21
|
+
case macro
|
22
|
+
when :belongs_to
|
23
|
+
@parent_columns = foreign_key(assoc)
|
24
|
+
@child_columns = reference_key(assoc)
|
25
|
+
when :has_many, :has_one
|
26
|
+
if through_assoc = assoc.through_reflection
|
27
|
+
@through = assoc.source_reflection.name
|
28
|
+
|
29
|
+
assoc = through_assoc
|
30
|
+
options = {:only => [], :include => {@through => options}}
|
31
|
+
end
|
32
|
+
|
33
|
+
@parent_columns = reference_key(assoc)
|
34
|
+
@child_columns = foreign_key(assoc)
|
35
|
+
else
|
36
|
+
raise "currently unsupported association macro: #{macro}"
|
37
|
+
end
|
38
|
+
|
39
|
+
klass = assoc.klass
|
40
|
+
@child_node = Node.new(assoc.klass, options)
|
41
|
+
@table_name = klass.table_name
|
42
|
+
@connection = klass.connection
|
43
|
+
end
|
44
|
+
|
45
|
+
def node
|
46
|
+
through ? child_node[through] : child_node
|
47
|
+
end
|
48
|
+
|
49
|
+
def parent_id(record)
|
50
|
+
record.send parent_columns.at(0)
|
51
|
+
end
|
52
|
+
|
53
|
+
def child_id(record)
|
54
|
+
record.send child_columns.at(0)
|
55
|
+
end
|
56
|
+
|
57
|
+
def conditions(id_map)
|
58
|
+
["#{table_name}.#{connection.quote_column_name(child_columns.at(0))} IN (?)", id_map.keys.flatten]
|
59
|
+
end
|
60
|
+
|
61
|
+
def link(parents)
|
62
|
+
id_map = Hash.new {|hash, key| hash[key] = [] }
|
63
|
+
|
64
|
+
parents = arrayify(parents)
|
65
|
+
parents.each do |parent|
|
66
|
+
id_map[parent_id(parent)] << parent
|
67
|
+
end
|
68
|
+
|
69
|
+
children = child_node.find(:all,
|
70
|
+
:select => child_columns,
|
71
|
+
:conditions => conditions(id_map))
|
72
|
+
visited = []
|
73
|
+
|
74
|
+
arrayify(children).each do |child|
|
75
|
+
id_map[child_id(child)].each do |parent|
|
76
|
+
visited << parent
|
77
|
+
set_child(parent, child)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
if macro == :has_many && through
|
82
|
+
visited.each do |parent|
|
83
|
+
parent.send(name).uniq!
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
(parents - visited).each do |parent|
|
88
|
+
set_child(parent, nil)
|
89
|
+
end
|
90
|
+
|
91
|
+
children
|
92
|
+
end
|
93
|
+
|
94
|
+
def inherit(method_name, paths)
|
95
|
+
dup.inherit!(method_name, paths)
|
96
|
+
end
|
97
|
+
|
98
|
+
def inherit!(method_name, paths)
|
99
|
+
paths = paths.collect {|path| "#{through}.#{path}"} if through
|
100
|
+
@child_node = @child_node.send(method_name, paths)
|
101
|
+
self
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def arrayify(obj) # :nodoc:
|
107
|
+
obj.kind_of?(Array) ? obj : [obj]
|
108
|
+
end
|
109
|
+
|
110
|
+
def set_child(parent, child) # :nodoc:
|
111
|
+
if child && through
|
112
|
+
child = child.send(through).target
|
113
|
+
end
|
114
|
+
|
115
|
+
case macro
|
116
|
+
when :belongs_to, :has_one
|
117
|
+
parent.send("set_#{name}_target", child)
|
118
|
+
when :has_many
|
119
|
+
association_proxy = parent.send(name)
|
120
|
+
association_proxy.loaded
|
121
|
+
association_proxy.target.push(child) if child
|
122
|
+
else
|
123
|
+
# should never get here...
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,290 @@
|
|
1
|
+
require 'data_graph/cpk_linkage'
|
2
|
+
|
3
|
+
module DataGraph
|
4
|
+
|
5
|
+
#-- Treated as immutable once created.
|
6
|
+
class Node
|
7
|
+
include Utils
|
8
|
+
|
9
|
+
attr_reader :model
|
10
|
+
attr_reader :column_names
|
11
|
+
attr_reader :method_names
|
12
|
+
attr_reader :linkages
|
13
|
+
attr_reader :always_columns
|
14
|
+
|
15
|
+
def initialize(model, options={})
|
16
|
+
@model = model
|
17
|
+
self.options = options
|
18
|
+
end
|
19
|
+
|
20
|
+
def associations
|
21
|
+
linkages.keys
|
22
|
+
end
|
23
|
+
|
24
|
+
def [](name)
|
25
|
+
linkage = linkages[name.to_s]
|
26
|
+
linkage ? linkage.node : nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def paths
|
30
|
+
paths = column_names + method_names
|
31
|
+
linkages.each_pair do |name, linkage|
|
32
|
+
linkage.node.paths.each do |path|
|
33
|
+
paths << "#{name}.#{path}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
paths
|
38
|
+
end
|
39
|
+
|
40
|
+
def get_paths
|
41
|
+
paths = primary_keys(model) + column_names + method_names
|
42
|
+
linkages.each_pair do |name, linkage|
|
43
|
+
paths << name
|
44
|
+
paths.concat linkage.parent_columns
|
45
|
+
(linkage.node.get_paths + linkage.child_columns).each do |path|
|
46
|
+
paths << "#{name}.#{path}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
paths.uniq!
|
51
|
+
paths
|
52
|
+
end
|
53
|
+
|
54
|
+
def set_paths
|
55
|
+
paths = primary_keys(model) + column_names + method_names
|
56
|
+
nested_attributes = model.nested_attributes_options
|
57
|
+
linkages.each_pair do |name, linkage|
|
58
|
+
next unless nested_attributes.has_key?(name.to_sym)
|
59
|
+
|
60
|
+
attributes = linkage.node.set_paths
|
61
|
+
attributes += ActiveRecord::NestedAttributes::UNASSIGNABLE_KEYS
|
62
|
+
attributes.each do |path|
|
63
|
+
paths << "#{name}_attributes.#{path}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
paths.uniq!
|
68
|
+
paths
|
69
|
+
end
|
70
|
+
|
71
|
+
def aliases
|
72
|
+
aliases = {'*' => column_names.dup}
|
73
|
+
|
74
|
+
linkages.each_pair do |name, linkage|
|
75
|
+
linkage.node.aliases.each_pair do |als, paths|
|
76
|
+
aliases["#{name}.#{als}"] = paths.collect {|path| "#{name}.#{path}" }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
aliases
|
81
|
+
end
|
82
|
+
|
83
|
+
def nest_paths
|
84
|
+
paths = []
|
85
|
+
|
86
|
+
linkages.each_pair do |name, linkage|
|
87
|
+
if linkage.macro == :has_many
|
88
|
+
paths << "#{name}_attributes"
|
89
|
+
end
|
90
|
+
|
91
|
+
linkage.node.nest_paths.each do |path|
|
92
|
+
paths << "#{name}.#{path}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
paths
|
97
|
+
end
|
98
|
+
|
99
|
+
def options
|
100
|
+
associations = {}
|
101
|
+
linkages.each_pair do |name, linkage|
|
102
|
+
associations[name.to_sym] = linkage.node.options
|
103
|
+
end
|
104
|
+
|
105
|
+
# note a new options hash must be generated because serialization is
|
106
|
+
# destructive to the hash (although not the values)
|
107
|
+
|
108
|
+
{
|
109
|
+
:only => column_names,
|
110
|
+
:methods => method_names,
|
111
|
+
:include => associations
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
def options=(options)
|
116
|
+
unless options.kind_of?(Hash)
|
117
|
+
raise "not a hash: #{options.inspect}"
|
118
|
+
end
|
119
|
+
@column_names = parse_columns(options)
|
120
|
+
@method_names = parse_methods(options)
|
121
|
+
@linkages = parse_linkages(options)
|
122
|
+
@always_columns = parse_always(options)
|
123
|
+
end
|
124
|
+
|
125
|
+
def find(*args)
|
126
|
+
link model.find(*find_args(args))
|
127
|
+
end
|
128
|
+
|
129
|
+
def paginate(*args)
|
130
|
+
link model.paginate(*find_args(args))
|
131
|
+
end
|
132
|
+
|
133
|
+
def find_args(args=[])
|
134
|
+
args << {} unless args.last.kind_of?(Hash)
|
135
|
+
scope(args.last)
|
136
|
+
args
|
137
|
+
end
|
138
|
+
|
139
|
+
def scope(options={})
|
140
|
+
columns = arrayify(options[:select]) + column_names + always_columns
|
141
|
+
linkages.each_value {|linkage| columns.concat linkage.parent_columns }
|
142
|
+
columns.uniq!
|
143
|
+
|
144
|
+
options[:select] = columns.join(',')
|
145
|
+
options
|
146
|
+
end
|
147
|
+
|
148
|
+
def link(records)
|
149
|
+
linkages.each_value do |linkage|
|
150
|
+
linkage.link(records)
|
151
|
+
end
|
152
|
+
|
153
|
+
records
|
154
|
+
end
|
155
|
+
|
156
|
+
def only!(paths)
|
157
|
+
attr_paths, nest_paths = partition(paths)
|
158
|
+
source, target = linkages, {}
|
159
|
+
|
160
|
+
attr_paths.each do |name|
|
161
|
+
if linkage = source[name]
|
162
|
+
target[name] = linkage
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
nest_paths.each_pair do |name, paths|
|
167
|
+
if linkage = source[name]
|
168
|
+
target[name] = linkage.inherit(:only, paths)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
@column_names &= attr_paths
|
173
|
+
@method_names &= attr_paths
|
174
|
+
@linkages = target
|
175
|
+
|
176
|
+
self
|
177
|
+
end
|
178
|
+
|
179
|
+
def only(paths)
|
180
|
+
dup.only!(paths)
|
181
|
+
end
|
182
|
+
|
183
|
+
def except!(paths)
|
184
|
+
attr_paths, nest_paths = partition(paths)
|
185
|
+
source, target = linkages, {}
|
186
|
+
|
187
|
+
(attr_paths - nest_paths.keys).each do |path|
|
188
|
+
source.delete(path)
|
189
|
+
end
|
190
|
+
|
191
|
+
nest_paths.each_pair do |name, paths|
|
192
|
+
if linkage = source[name]
|
193
|
+
target[name] = linkage.inherit(:except, paths)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
@column_names -= attr_paths
|
198
|
+
@method_names -= attr_paths
|
199
|
+
@linkages = target
|
200
|
+
|
201
|
+
self
|
202
|
+
end
|
203
|
+
|
204
|
+
def except(paths)
|
205
|
+
dup.except!(paths)
|
206
|
+
end
|
207
|
+
|
208
|
+
private
|
209
|
+
|
210
|
+
def arrayify(array)
|
211
|
+
case array
|
212
|
+
when Array then array
|
213
|
+
when nil then []
|
214
|
+
else [array]
|
215
|
+
end.collect! {|obj| obj.to_s }
|
216
|
+
end
|
217
|
+
|
218
|
+
def parse_columns(options)
|
219
|
+
attributes = model.column_names
|
220
|
+
|
221
|
+
case
|
222
|
+
when options.has_key?(:except)
|
223
|
+
if options.has_key?(:only)
|
224
|
+
raise "only and except are both specified: #{options.inspect}"
|
225
|
+
end
|
226
|
+
|
227
|
+
except = options[:except]
|
228
|
+
attributes - arrayify(except)
|
229
|
+
|
230
|
+
when options.has_key?(:only)
|
231
|
+
only = options[:only]
|
232
|
+
arrayify(only) & attributes
|
233
|
+
|
234
|
+
else
|
235
|
+
attributes.dup
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def parse_methods(options)
|
240
|
+
arrayify(options[:methods])
|
241
|
+
end
|
242
|
+
|
243
|
+
def hashify(hash)
|
244
|
+
case hash
|
245
|
+
when Hash # default
|
246
|
+
hash.symbolize_keys
|
247
|
+
when Array # an array of identifiers {:include => [:a, :b]}
|
248
|
+
hash.inject({}) {|h, k| h[k.to_sym] = {}; h}
|
249
|
+
else # a bare identifier {:include => :a}
|
250
|
+
{hash.to_sym => {}}
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def parse_linkages(options)
|
255
|
+
linkages = {}
|
256
|
+
|
257
|
+
hashify(options[:include] || {}).each_pair do |name, options|
|
258
|
+
next unless assoc = model.reflect_on_association(name)
|
259
|
+
linkage = cpk?(assoc) ? CpkLinkage : Linkage
|
260
|
+
linkages[name.to_s] = linkage.new(assoc, options)
|
261
|
+
end
|
262
|
+
|
263
|
+
linkages
|
264
|
+
end
|
265
|
+
|
266
|
+
def parse_always(options)
|
267
|
+
always_columns = primary_keys(model).collect {|key| key.to_s }
|
268
|
+
always_columns.concat arrayify(options[:always])
|
269
|
+
always_columns.uniq!
|
270
|
+
always_columns
|
271
|
+
end
|
272
|
+
|
273
|
+
def partition(paths)
|
274
|
+
attrs = []
|
275
|
+
nested_attrs = {}
|
276
|
+
|
277
|
+
paths.each do |path|
|
278
|
+
head, tail = path.split('.', 2)
|
279
|
+
|
280
|
+
if tail
|
281
|
+
(nested_attrs[head] ||= []) << tail
|
282
|
+
else
|
283
|
+
attrs << head
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
[attrs, nested_attrs]
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
ActiveRecord.load_all!
|
3
|
+
|
4
|
+
require 'composite_primary_keys'
|
5
|
+
|
6
|
+
module DataGraph
|
7
|
+
module Utils
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def primary_keys(model)
|
11
|
+
model.respond_to?(:primary_keys) ? model.primary_keys : [model.primary_key]
|
12
|
+
end
|
13
|
+
|
14
|
+
def foreign_key(assoc)
|
15
|
+
# actually returns options[:foreign_key], or the default foreign key
|
16
|
+
foreign_key = assoc.primary_key_name
|
17
|
+
|
18
|
+
# cpk returns a csv string
|
19
|
+
foreign_key.to_s.split(',')
|
20
|
+
end
|
21
|
+
|
22
|
+
def reference_key(assoc)
|
23
|
+
primary_key = assoc.options[:primary_key] || primary_keys(assoc.macro == :belongs_to ? assoc.klass : assoc.active_record)
|
24
|
+
primary_key.kind_of?(Array) ? primary_key.collect {|key| key.to_s } : primary_key.to_s.split(',')
|
25
|
+
end
|
26
|
+
|
27
|
+
def cpk?(assoc)
|
28
|
+
assoc = assoc.through_reflection if assoc.through_reflection
|
29
|
+
assoc.primary_key_name.to_s.include?(',')
|
30
|
+
end
|
31
|
+
|
32
|
+
def patherize_attrs(attrs, nest_paths=[], paths=[], prefix='')
|
33
|
+
attrs.each_pair do |key, value|
|
34
|
+
case key
|
35
|
+
when String, Symbol
|
36
|
+
path = "#{prefix}#{key}"
|
37
|
+
|
38
|
+
if nest_paths.include?(path)
|
39
|
+
value = value.values
|
40
|
+
end
|
41
|
+
|
42
|
+
case value
|
43
|
+
when Hash
|
44
|
+
patherize_attrs(value, nest_paths, paths, "#{path}.")
|
45
|
+
when Array
|
46
|
+
next_prefix = "#{path}."
|
47
|
+
value.each {|hash| patherize_attrs(hash, nest_paths, paths, next_prefix) }
|
48
|
+
else
|
49
|
+
paths << path
|
50
|
+
end
|
51
|
+
else
|
52
|
+
raise "unexpected attribute key: #{key.inspect}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
paths
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: data_graph
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 1
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
version: 1.0.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Simon Chiang
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-07-20 00:00:00 -06:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: activerecord
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 2
|
29
|
+
- 3
|
30
|
+
- 5
|
31
|
+
version: 2.3.5
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: composite_primary_keys
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 2
|
43
|
+
- 3
|
44
|
+
- 5
|
45
|
+
- 1
|
46
|
+
version: 2.3.5.1
|
47
|
+
type: :runtime
|
48
|
+
version_requirements: *id002
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: sqlite3-ruby
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ~>
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
segments:
|
57
|
+
- 1
|
58
|
+
- 2
|
59
|
+
- 5
|
60
|
+
version: 1.2.5
|
61
|
+
type: :development
|
62
|
+
version_requirements: *id003
|
63
|
+
description:
|
64
|
+
email: simon.a.chiang@gmail.com
|
65
|
+
executables: []
|
66
|
+
|
67
|
+
extensions: []
|
68
|
+
|
69
|
+
extra_rdoc_files:
|
70
|
+
- History
|
71
|
+
- README
|
72
|
+
- License.txt
|
73
|
+
files:
|
74
|
+
- lib/data_graph.rb
|
75
|
+
- lib/data_graph/cpk_linkage.rb
|
76
|
+
- lib/data_graph/graph.rb
|
77
|
+
- lib/data_graph/linkage.rb
|
78
|
+
- lib/data_graph/node.rb
|
79
|
+
- lib/data_graph/utils.rb
|
80
|
+
- lib/data_graph/version.rb
|
81
|
+
- History
|
82
|
+
- README
|
83
|
+
- License.txt
|
84
|
+
has_rdoc: true
|
85
|
+
homepage: ""
|
86
|
+
licenses: []
|
87
|
+
|
88
|
+
post_install_message:
|
89
|
+
rdoc_options:
|
90
|
+
- --main
|
91
|
+
- README
|
92
|
+
- -S
|
93
|
+
- -N
|
94
|
+
- --title
|
95
|
+
- Data Graph
|
96
|
+
require_paths:
|
97
|
+
- lib
|
98
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
segments:
|
103
|
+
- 0
|
104
|
+
version: "0"
|
105
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
segments:
|
110
|
+
- 0
|
111
|
+
version: "0"
|
112
|
+
requirements: []
|
113
|
+
|
114
|
+
rubyforge_project: ""
|
115
|
+
rubygems_version: 1.3.6
|
116
|
+
signing_key:
|
117
|
+
specification_version: 3
|
118
|
+
summary: ""
|
119
|
+
test_files: []
|
120
|
+
|