google_data_source 0.7.6
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/.document +5 -0
- data/.gitignore +7 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +25 -0
- data/Rakefile +31 -0
- data/google_data_source.gemspec +32 -0
- data/lib/assets/images/google_data_source/chart_bar_add.png +0 -0
- data/lib/assets/images/google_data_source/chart_bar_delete.png +0 -0
- data/lib/assets/images/google_data_source/loader.gif +0 -0
- data/lib/assets/javascripts/google_data_source/data_source_init.js +3 -0
- data/lib/assets/javascripts/google_data_source/extended_data_table.js +76 -0
- data/lib/assets/javascripts/google_data_source/filter_form.js +180 -0
- data/lib/assets/javascripts/google_data_source/google_visualization/combo_table.js.erb +113 -0
- data/lib/assets/javascripts/google_data_source/google_visualization/table.js +116 -0
- data/lib/assets/javascripts/google_data_source/google_visualization/timeline.js +13 -0
- data/lib/assets/javascripts/google_data_source/google_visualization/visualization.js.erb +141 -0
- data/lib/assets/javascripts/google_data_source/index.js +7 -0
- data/lib/dummy_engine.rb +5 -0
- data/lib/google_data_source.rb +33 -0
- data/lib/google_data_source/base.rb +281 -0
- data/lib/google_data_source/column.rb +31 -0
- data/lib/google_data_source/csv_data.rb +23 -0
- data/lib/google_data_source/data_date.rb +17 -0
- data/lib/google_data_source/data_date_time.rb +17 -0
- data/lib/google_data_source/helper.rb +69 -0
- data/lib/google_data_source/html_data.rb +6 -0
- data/lib/google_data_source/invalid_data.rb +14 -0
- data/lib/google_data_source/json_data.rb +78 -0
- data/lib/google_data_source/railtie.rb +36 -0
- data/lib/google_data_source/sql/models.rb +266 -0
- data/lib/google_data_source/sql/parser.rb +239 -0
- data/lib/google_data_source/sql_parser.rb +82 -0
- data/lib/google_data_source/template_handler.rb +31 -0
- data/lib/google_data_source/test_helper.rb +26 -0
- data/lib/google_data_source/version.rb +3 -0
- data/lib/google_data_source/xml_data.rb +25 -0
- data/lib/locale/de.yml +5 -0
- data/lib/reporting/action_controller_extension.rb +19 -0
- data/lib/reporting/grouped_set.rb +58 -0
- data/lib/reporting/helper.rb +110 -0
- data/lib/reporting/reporting.rb +352 -0
- data/lib/reporting/reporting_adapter.rb +27 -0
- data/lib/reporting/reporting_entry.rb +147 -0
- data/lib/reporting/sql_reporting.rb +220 -0
- data/test/lib/empty_reporting.rb +2 -0
- data/test/lib/test_reporting.rb +33 -0
- data/test/lib/test_reporting_b.rb +9 -0
- data/test/lib/test_reporting_c.rb +3 -0
- data/test/locales/en.models.yml +6 -0
- data/test/locales/en.reportings.yml +5 -0
- data/test/rails/reporting_renderer_test.rb +47 -0
- data/test/test_helper.rb +50 -0
- data/test/units/base_test.rb +340 -0
- data/test/units/csv_data_test.rb +36 -0
- data/test/units/grouped_set_test.rb +60 -0
- data/test/units/json_data_test.rb +68 -0
- data/test/units/reporting_adapter_test.rb +20 -0
- data/test/units/reporting_entry_test.rb +149 -0
- data/test/units/reporting_test.rb +374 -0
- data/test/units/sql_parser_test.rb +111 -0
- data/test/units/sql_reporting_test.rb +307 -0
- data/test/units/xml_data_test.rb +32 -0
- metadata +286 -0
@@ -0,0 +1,239 @@
|
|
1
|
+
module GoogleDataSource
|
2
|
+
module DataSource
|
3
|
+
module Sql
|
4
|
+
class ::Method; include RParsec::FunctorMixin; end
|
5
|
+
class ::Proc; include RParsec::FunctorMixin; end
|
6
|
+
|
7
|
+
module Parser
|
8
|
+
include RParsec
|
9
|
+
include Functors
|
10
|
+
include Parsers
|
11
|
+
|
12
|
+
extend Parsers
|
13
|
+
# TODO drop keywords
|
14
|
+
MyKeywords = Keywords.case_insensitive(%w{
|
15
|
+
select from where group by having order desc asc
|
16
|
+
inner left right full outer inner join on cross
|
17
|
+
union all distinct as exists in between limit offset
|
18
|
+
case when else end and or not true false
|
19
|
+
})
|
20
|
+
MyOperators = Operators.new(%w{+ - * / % = > < >= <= <> != : ( ) . ,})
|
21
|
+
def self.operators(*ops)
|
22
|
+
result = []
|
23
|
+
ops.each do |op|
|
24
|
+
result << (MyOperators[op] >> op.to_sym)
|
25
|
+
end
|
26
|
+
sum(*result)
|
27
|
+
end
|
28
|
+
Comparators = operators(*%w{= > < >= <= <> !=})
|
29
|
+
quote_mapper = Proc.new do |raw|
|
30
|
+
# is this really different to raw.gsub! ???
|
31
|
+
raw.replace(raw.gsub(/\\'/, "'").gsub(/\\\\/, "\\"))
|
32
|
+
end
|
33
|
+
|
34
|
+
StringLiteral = char(?') >> ((str("\\\\")|str("\\'")|not_char(?')).many_.fragment).map("e_mapper) << char(?')
|
35
|
+
QuotedName = char(?`) >> not_char(?`).many_.fragment << char(?`)
|
36
|
+
Variable = char(?$) >> word
|
37
|
+
MyLexer = number.token(:number) | StringLiteral.token(:string) | Variable.token(:var) |
|
38
|
+
QuotedName.token(:word) | MyKeywords.lexer | MyOperators.lexer
|
39
|
+
MyLexeme = MyLexer.lexeme(whitespaces | comment_line('#')) << eof
|
40
|
+
|
41
|
+
|
42
|
+
######################################### utilities #########################################
|
43
|
+
def keyword
|
44
|
+
MyKeywords
|
45
|
+
end
|
46
|
+
|
47
|
+
def operator
|
48
|
+
MyOperators
|
49
|
+
end
|
50
|
+
|
51
|
+
def comma
|
52
|
+
operator[',']
|
53
|
+
end
|
54
|
+
|
55
|
+
def list expr
|
56
|
+
paren(expr.delimited(comma))
|
57
|
+
end
|
58
|
+
|
59
|
+
def word(&block)
|
60
|
+
if block.nil?
|
61
|
+
token(:word, &Id)
|
62
|
+
else
|
63
|
+
token(:word, &block)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def paren parser
|
68
|
+
operator['('] >> parser << operator[')']
|
69
|
+
end
|
70
|
+
|
71
|
+
def ctor cls
|
72
|
+
cls.method :new
|
73
|
+
end
|
74
|
+
|
75
|
+
def rctor cls, arity=2
|
76
|
+
ctor(cls).reverse_curry arity
|
77
|
+
end
|
78
|
+
|
79
|
+
################################### predicate parser #############################
|
80
|
+
def logical_operator op
|
81
|
+
proc{|a,b|CompoundPredicate.new(a,op,b)}
|
82
|
+
end
|
83
|
+
|
84
|
+
def make_predicate expr, rel
|
85
|
+
expr_list = list expr
|
86
|
+
comparison = make_comparison_predicate expr, rel
|
87
|
+
group_comparison = sequence(expr_list, Comparators, expr_list, &ctor(GroupComparisonPredicate))
|
88
|
+
bool = nil
|
89
|
+
lazy_bool = lazy{bool}
|
90
|
+
bool_term = keyword[:true] >> true | keyword[:false] >> false |
|
91
|
+
comparison | group_comparison | paren(lazy_bool) |
|
92
|
+
make_exists(rel) | make_not_exists(rel)
|
93
|
+
bool_table = OperatorTable.new.
|
94
|
+
infixl(keyword[:or] >> logical_operator(:or), 20).
|
95
|
+
infixl(keyword[:and] >> logical_operator(:and), 30).
|
96
|
+
prefix(keyword[:not] >> ctor(NotPredicate), 40)
|
97
|
+
bool = Expressions.build(bool_term, bool_table)
|
98
|
+
end
|
99
|
+
|
100
|
+
def make_exists rel
|
101
|
+
keyword[:exists] >> rel.map(&ctor(ExistsPredicate))
|
102
|
+
end
|
103
|
+
|
104
|
+
def make_not_exists rel
|
105
|
+
keyword[:not] >> keyword[:exists] >> rel.map(&ctor(NotExistsPredicate))
|
106
|
+
end
|
107
|
+
|
108
|
+
def make_in expr
|
109
|
+
keyword[:in] >> list(expr) >> map(&rctor(InPredicate))
|
110
|
+
end
|
111
|
+
|
112
|
+
def make_not_in expr
|
113
|
+
keyword[:not] >> keyword[:in] >> list(expr) >> map(&rctor(NotInPredicate))
|
114
|
+
end
|
115
|
+
|
116
|
+
def make_in_relation rel
|
117
|
+
keyword[:in] >> rel.map(&rctor(InRelationPredicate))
|
118
|
+
end
|
119
|
+
|
120
|
+
def make_not_in_relation rel
|
121
|
+
keyword[:not] >> keyword[:in] >> rel.map(&rctor(NotInRelationPredicate))
|
122
|
+
end
|
123
|
+
|
124
|
+
def make_between expr
|
125
|
+
make_between_clause(expr, &ctor(BetweenPredicate))
|
126
|
+
end
|
127
|
+
|
128
|
+
def make_not_between expr
|
129
|
+
keyword[:not] >> make_between_clause(expr, &ctor(NotBetweenPredicate))
|
130
|
+
end
|
131
|
+
|
132
|
+
def make_comparison_predicate expr, rel
|
133
|
+
comparison = sequence(Comparators, expr) do |op,e2|
|
134
|
+
proc{|e1|ComparePredicate.new(e1, op, e2)}
|
135
|
+
end
|
136
|
+
in_clause = make_in expr
|
137
|
+
not_in_clause = make_not_in expr
|
138
|
+
in_relation = make_in_relation rel
|
139
|
+
not_in_relation = make_not_in_relation rel
|
140
|
+
between = make_between expr
|
141
|
+
not_between = make_not_between expr
|
142
|
+
compare_with = comparison | in_clause | not_in_clause |
|
143
|
+
in_relation | not_in_relation | between | not_between
|
144
|
+
sequence(expr, compare_with, &Feed)
|
145
|
+
end
|
146
|
+
|
147
|
+
def make_between_clause expr, &maker
|
148
|
+
factory = proc do |a,_,b|
|
149
|
+
proc {|v|maker.call(v,a,b)}
|
150
|
+
end
|
151
|
+
variant1 = keyword[:between] >> paren(sequence(expr, comma, expr, &factory))
|
152
|
+
variant2 = keyword[:between] >> sequence(expr, keyword[:and], expr, &factory)
|
153
|
+
variant1 | variant2
|
154
|
+
end
|
155
|
+
|
156
|
+
################################ expression parser ###############################
|
157
|
+
def calculate_simple_cases(val, cases, default)
|
158
|
+
SimpleCaseExpr.new(val, cases, default)
|
159
|
+
end
|
160
|
+
|
161
|
+
def calculate_full_cases(cases, default)
|
162
|
+
CaseExpr.new(cases, default)
|
163
|
+
end
|
164
|
+
|
165
|
+
def make_expression predicate, rel
|
166
|
+
expr = nil
|
167
|
+
lazy_expr = lazy{expr}
|
168
|
+
|
169
|
+
wildcard = operator[:*] >> WildcardExpr::Instance
|
170
|
+
lit = token(:number, :string, &ctor(LiteralExpr)) | token(:var, &ctor(VarExpr))
|
171
|
+
atom = lit | wildcard | word(&ctor(WordExpr))
|
172
|
+
term = atom | (operator['('] >> lazy_expr << operator[')'])
|
173
|
+
|
174
|
+
table = OperatorTable.new.
|
175
|
+
infixl(operator['+'] >> Plus, 20).
|
176
|
+
infixl(operator['-'] >> Minus, 20).
|
177
|
+
infixl(operator['*'] >> Mul, 30).
|
178
|
+
infixl(operator['/'] >> Div, 30).
|
179
|
+
infixl(operator['%'] >> Mod, 30).
|
180
|
+
prefix(operator['-'] >> Neg, 50)
|
181
|
+
expr = Expressions.build(term, table)
|
182
|
+
end
|
183
|
+
|
184
|
+
################################ relation parser ###############################
|
185
|
+
def make_relation expr, pred
|
186
|
+
where_clause = keyword[:where] >> pred
|
187
|
+
order_element = sequence(expr, (keyword[:asc] >> true | keyword[:desc] >> false).optional(true),
|
188
|
+
&ctor(OrderElement))
|
189
|
+
order_elements = order_element.separated1(comma)
|
190
|
+
exprs = expr.separated1(comma)
|
191
|
+
|
192
|
+
# setup clauses
|
193
|
+
select_clause = keyword[:select] >> exprs
|
194
|
+
order_by_clause = keyword[:order] >> keyword[:by] >> order_elements
|
195
|
+
group_by = keyword[:group] >> keyword[:by] >> exprs
|
196
|
+
group_by_clause = sequence(group_by, (keyword[:having] >> pred).optional, &ctor(GroupByClause))
|
197
|
+
limit_clause = keyword[:limit] >> token(:number, &To_i)
|
198
|
+
offset_clause = keyword[:offset] >> token(:number, &To_i)
|
199
|
+
|
200
|
+
# build relation
|
201
|
+
relation = sequence(
|
202
|
+
select_clause.optional([WildcardExpr.new]),
|
203
|
+
where_clause.optional, group_by_clause.optional, order_by_clause.optional,
|
204
|
+
limit_clause.optional, offset_clause.optional
|
205
|
+
) do |select, where, groupby, orderby, limit, offset|
|
206
|
+
SelectRelation.new(select, where, groupby, orderby, limit, offset)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
########################## put together ###############################
|
211
|
+
def expression
|
212
|
+
assemble[0]
|
213
|
+
end
|
214
|
+
|
215
|
+
def relation
|
216
|
+
assemble[2]
|
217
|
+
end
|
218
|
+
def predicate
|
219
|
+
assemble[1]
|
220
|
+
end
|
221
|
+
|
222
|
+
def assemble
|
223
|
+
pred = nil
|
224
|
+
rel = nil
|
225
|
+
lazy_predicate = lazy{pred}
|
226
|
+
lazy_rel = lazy{rel}
|
227
|
+
expr = make_expression lazy_predicate, lazy_rel
|
228
|
+
pred = make_predicate expr, lazy_rel
|
229
|
+
rel = make_relation expr, pred
|
230
|
+
return expr, pred, rel
|
231
|
+
end
|
232
|
+
|
233
|
+
def make parser
|
234
|
+
MyLexeme.nested(parser << eof)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module GoogleDataSource
|
2
|
+
module DataSource
|
3
|
+
class SimpleSqlException < Exception; end
|
4
|
+
|
5
|
+
class SqlParser
|
6
|
+
include Sql::Parser
|
7
|
+
|
8
|
+
class << self
|
9
|
+
# Parses a simple SQL query and return the result
|
10
|
+
# The where statement may only contain equality comparisons that are connected with 'and'
|
11
|
+
# Only a single ordering parameter is accepted
|
12
|
+
# Throws a +SimpleSqlException+ if these conditions are not satisfied
|
13
|
+
def simple_parse(query)
|
14
|
+
result = parse(query)
|
15
|
+
OpenStruct.new({
|
16
|
+
:select => result.select.collect(&:to_s),
|
17
|
+
:conditions => simple_where_parser(result.where),
|
18
|
+
:orderby => simple_orderby_parser(result.orderby),
|
19
|
+
:groupby => simple_groupby_parser(result.groupby),
|
20
|
+
:limit => result.limit,
|
21
|
+
:offset => result.offset
|
22
|
+
})
|
23
|
+
end
|
24
|
+
|
25
|
+
# Parses a SQL query and returns the result
|
26
|
+
def parse(query)
|
27
|
+
parser.parse(query)
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
# Helper to the +simple_parse+ method
|
32
|
+
def simple_where_parser(predicate, result = Hash.new)
|
33
|
+
case predicate.class.name.split('::').last
|
34
|
+
when 'CompoundPredicate'
|
35
|
+
raise SimpleSqlException.new("Operator forbidden (use only 'and')") unless predicate.op == :and
|
36
|
+
simple_where_parser(predicate.left, result)
|
37
|
+
simple_where_parser(predicate.right, result)
|
38
|
+
when 'ComparePredicate'
|
39
|
+
case predicate.op
|
40
|
+
when :"="
|
41
|
+
result[predicate.left.to_s] = predicate.right.to_s
|
42
|
+
when :"<", :">", :">=", :"<=", :"<>", :"!="
|
43
|
+
result[predicate.left.to_s] ||= Array.new
|
44
|
+
raise SimpleSqlException.new("Condition clach") unless result[predicate.left.to_s].is_a?(Array)
|
45
|
+
result[predicate.left.to_s] << OpenStruct.new(:op => predicate.op.to_s, :value => predicate.right.to_s)
|
46
|
+
else
|
47
|
+
raise SimpleSqlException.new("Comparator forbidden (use only '=,<,>')") unless predicate.op == :"="
|
48
|
+
end
|
49
|
+
when 'InPredicate'
|
50
|
+
result[predicate.expr.to_s] ||= Array.new
|
51
|
+
raise SimpleSqlException.new("Condition clach") unless result[predicate.expr.to_s].is_a?(Array)
|
52
|
+
result[predicate.expr.to_s] << OpenStruct.new(:op => 'in', :value => predicate.vals.map(&:to_s))
|
53
|
+
when 'NilClass'
|
54
|
+
# do nothing
|
55
|
+
else
|
56
|
+
raise SimpleSqlException.new("Unknown syntax error")
|
57
|
+
end
|
58
|
+
result
|
59
|
+
end
|
60
|
+
|
61
|
+
# Helper to the +simple_parse+ method
|
62
|
+
def simple_orderby_parser(orderby)
|
63
|
+
return nil if orderby.nil?
|
64
|
+
raise SimpleSqlException.new("Too many ordering arguments (1 allowed)") if orderby.size > 1
|
65
|
+
[orderby.first.expr.to_s, orderby.first.asc ? :asc : :desc]
|
66
|
+
end
|
67
|
+
|
68
|
+
# Helper to the +simple_parse+ method
|
69
|
+
def simple_groupby_parser(groupby)
|
70
|
+
return [] if groupby.nil?
|
71
|
+
groupby.exprs.collect(&:to_s)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns the parser
|
75
|
+
def parser
|
76
|
+
sql = self.new
|
77
|
+
sql.make(sql.relation)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module GoogleDataSource
|
2
|
+
module DataSource
|
3
|
+
# A simple template handler for a data source
|
4
|
+
# Provides a GoogleDataSource::Base object datasource in the template so the template
|
5
|
+
# can fill it with data
|
6
|
+
class TemplateHandler
|
7
|
+
|
8
|
+
def self.call(template)
|
9
|
+
<<-EOT
|
10
|
+
datasource = GoogleDataSource::DataSource::Base.from_params(params)
|
11
|
+
#{template.source.dup}
|
12
|
+
if !datasource.reporting.nil? && datasource.reporting.has_form?
|
13
|
+
datasource.callback = "$('\\\#\#{datasource.reporting.form_id}').html(\#{render(:partial => datasource.reporting.partial).to_json});"
|
14
|
+
end
|
15
|
+
|
16
|
+
if datasource.format == 'csv'
|
17
|
+
headers['Content-Type'] = 'text/csv; charset=utf-8'
|
18
|
+
headers['Content-Disposition'] = "attachment; filename=\\"\#{datasource.export_filename}.csv\\""
|
19
|
+
end
|
20
|
+
|
21
|
+
if datasource.format == 'xml'
|
22
|
+
headers['Content-Type'] = 'application/xml; charset=utf-8'
|
23
|
+
headers['Content-Disposition'] = "attachment; filename=\\"\#{datasource.export_filename}.xml\\""
|
24
|
+
end
|
25
|
+
|
26
|
+
datasource.response
|
27
|
+
EOT
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module GoogleDataSource
|
2
|
+
# Some usefull helpers for testing datasources
|
3
|
+
module TestHelper
|
4
|
+
|
5
|
+
# Returns the parsed JSON argument of the datasource response
|
6
|
+
def datasource_response
|
7
|
+
first_cmd = @response.body.match(/([^;"]*|"(\\"|[^"])*;?(\\"|[^"])*")*;/)[0]
|
8
|
+
response = OpenStruct.new(JSON.parse(first_cmd.match(/^[^(]*\((.*)\);$/)[1]))
|
9
|
+
response.table = OpenStruct.new(response.table)
|
10
|
+
response
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns the columns array of the JSON response
|
14
|
+
def datasource_column(column)
|
15
|
+
response = datasource_response
|
16
|
+
column_no = response.table.cols.collect { |c| c['id'] }.index(column.to_s)
|
17
|
+
response.table.rows.collect { |r| r['c'][column_no] }
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the column ids of the JSON response
|
21
|
+
def datasource_column_ids
|
22
|
+
datasource_response.table.cols.collect { |c| c['id'] }
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
module GoogleDataSource
|
4
|
+
module DataSource
|
5
|
+
class XmlData < Base
|
6
|
+
#include ActionView::Helpers::NumberHelper
|
7
|
+
def response
|
8
|
+
cols = columns.map { |col| col.id || col.type }
|
9
|
+
builder = ::Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml|
|
10
|
+
xml.send(xml_class.pluralize, :type => 'array') do
|
11
|
+
data.each do |datarow|
|
12
|
+
xml.send(xml_class) do
|
13
|
+
datarow.zip(cols).each do |val, key|
|
14
|
+
xml.send("#{key}", val.is_a?(Hash) ? val[:v] : val)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
builder.to_xml
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/locale/de.yml
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module GoogleDataSource
|
2
|
+
module Reporting
|
3
|
+
module ActionControllerExtension
|
4
|
+
# Add the option +:reporting+ to the +render+ method.
|
5
|
+
# Takes a +Reporting+ object and renders the data-source response
|
6
|
+
# including the form validation results
|
7
|
+
def render_with_reporting(*args)
|
8
|
+
if !args.first.nil? && args.first.is_a?(Hash) && args.first.has_key?(:reporting)
|
9
|
+
reporting = args.first[:reporting]
|
10
|
+
datasource = GoogleDataSource::DataSource::Base.from_params(params)
|
11
|
+
datasource.set(reporting)
|
12
|
+
render_for_text datasource.response
|
13
|
+
else
|
14
|
+
render_without_reporting(*args)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# Represents an array of Objects which can be grouped according to grouping keys
|
2
|
+
# or a grouping block, which returns the hash used for grouping
|
3
|
+
#
|
4
|
+
# The objects must support the '+' operation which is used to implode sets of objects
|
5
|
+
# with the same grouping hash.
|
6
|
+
class GroupedSet < Array
|
7
|
+
#attr_reader :data
|
8
|
+
|
9
|
+
# Standard constructor
|
10
|
+
# +data+ is an Array of Objects implementing a '+' operation
|
11
|
+
#def initialize(data)
|
12
|
+
# @data = data
|
13
|
+
#end
|
14
|
+
|
15
|
+
# Bang method for the regrouping of the data
|
16
|
+
# The method takes either a set of keys or a block as grouping criterion
|
17
|
+
#
|
18
|
+
# If keys are set the grouping hash is built by calling +object.send(key).to_s+
|
19
|
+
# and concatening for all keys. (e.g. if +Object+ supports the method +date+ the
|
20
|
+
# grouping by +date+ is calculated by calling +regroup!(:date)+
|
21
|
+
#
|
22
|
+
# If a block is passed, it is called for every entry and it's result is taken as
|
23
|
+
# grouping hash. (e.g. +regroup! { |entry| entry.date }+ for grouping by date
|
24
|
+
#
|
25
|
+
# Objects with identical grouping hash are collapsed by calling the '+' operator
|
26
|
+
# on them.
|
27
|
+
#
|
28
|
+
# ATTENTION:
|
29
|
+
# This method won't recognize senseless grouping (e.g. calling
|
30
|
+
# +regroup! { |entry| entry.date.month }+ followed by
|
31
|
+
# +regroup! { |entry| entry.date }+ )
|
32
|
+
def regroup(*keys, &block)
|
33
|
+
# handle block
|
34
|
+
return regroup_by_proc(block) if block_given?
|
35
|
+
|
36
|
+
# handle keys
|
37
|
+
block = Proc.new do |entry|
|
38
|
+
keys.inject([]) { |memo, column| memo.push entry.send(column.to_sym).to_s }.join('-')
|
39
|
+
end
|
40
|
+
regroup_by_proc(block)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Collapse the collection to a single entry
|
44
|
+
def collapse
|
45
|
+
regroup.first
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
# Helper method for regroup with groups the data using a proc as grouping hash generator
|
50
|
+
def regroup_by_proc(grouping_proc)
|
51
|
+
data = self.group_by { |entry| grouping_proc.call(entry) }.values
|
52
|
+
result = data.collect do |entries|
|
53
|
+
# Uses the class of the first element to build the composite element
|
54
|
+
entries.first.class.composite(entries)
|
55
|
+
end
|
56
|
+
self.class.new(result)
|
57
|
+
end
|
58
|
+
end
|