data_graph 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/History ADDED
@@ -0,0 +1,3 @@
1
+ == 1.0.0 2010/07/20
2
+
3
+ Initial public release.
@@ -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]
@@ -0,0 +1,9 @@
1
+ require 'data_graph/graph'
2
+
3
+ module DataGraph
4
+ def data_graph(options={})
5
+ Graph.new(Node.new(self, options), options)
6
+ end
7
+ end
8
+
9
+ ActiveRecord::Base.extend(DataGraph)
@@ -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
@@ -0,0 +1,7 @@
1
+ module DataGraph
2
+ MAJOR = 1
3
+ MINOR = 0
4
+ TINY = 0
5
+
6
+ VERSION="#{MAJOR}.#{MINOR}.#{TINY}"
7
+ 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
+