mondrian-rest 0.5.0-java → 0.6.0-java

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fca6cfddd8cf42e7d87ec8e35f10b8389fdd08ff
4
- data.tar.gz: 08b4017a7cdc2eebc4f0b2218b51822396061dd5
3
+ metadata.gz: 2710889190b4d58cd7592415468caf4b7f5b7b95
4
+ data.tar.gz: 79bfcad96399941a47c1ba2e553e466209b053af
5
5
  SHA512:
6
- metadata.gz: 9fba6d67d771da283e085b0d89784baa093b5676b0c737aa89cdc5ef2b4ba1e9f3c9a68574db21b85759733e7f1161ec538e2051820bc641d446a62ac6577fd9
7
- data.tar.gz: 44ed30d9adf06ddfbb7fe157a8a2a6d5e1765936b82fbdbc2b24bbc12d2dbf88f23c41285d3c1f745034e82d4228d8b936128de6583c1e7e51b5d17d8fcacc59
6
+ metadata.gz: 81b615aa3796ac61ddd850dec37bfbcecd75a5565b3de5fad18acea59f212dae2445b2c4434d6d1910e9a5b433045cd46ef6d163b9182b314841b8d285006e1d
7
+ data.tar.gz: 6bbed693f2ba708a44688b9d85b0980afe343c52d7d9da2f26259e10d554b94b53e4abe88c89d77b2c39be453c6f532cb4dae2bf330e2ba0290f541d9c4beecf
data/Gemfile CHANGED
@@ -1,5 +1,6 @@
1
1
  source 'http://rubygems.org'
2
- gem 'mondrian-olap', :git => 'https://github.com/rsim/mondrian-olap.git', :branch => 'master'
3
2
  gem 'coveralls', require: false
3
+ gem 'ruby-debug'
4
+ gem 'mondrian-olap', :path => '/Users/manuel/Work/code/mondrian-olap', :require => 'mondrian-olap'
4
5
 
5
6
  gemspec
data/README.md CHANGED
@@ -10,7 +10,7 @@ See [`mondrian-rest-demo`](https://github.com/jazzido/mondrian-rest-demo) and [`
10
10
 
11
11
  ## Credits
12
12
 
13
- Initial inspiration for this project came from the [Stefan Urbanek](https://github.com/Stiivi)'s [`cubes`](http://cubes.databrewery.org/) OLAP server. `mondrian-rest` uses [Raimonds Simanovskis](https://github.com/rsim)'s fantastic [`mondrian-olap`](https://github.com/rsim/mondrian-olap) library.
13
+ Initial inspiration for this project came from [Stefan Urbanek](https://github.com/Stiivi)'s [`cubes`](http://cubes.databrewery.org/) OLAP server. `mondrian-rest` uses [Raimonds Simanovskis](https://github.com/rsim)'s fantastic [`mondrian-olap`](https://github.com/rsim/mondrian-olap) library.
14
14
 
15
15
 
16
16
  ## License
@@ -8,7 +8,7 @@ module Mondrian::REST
8
8
 
9
9
  class Api < Grape::API
10
10
  version '1', using: :header, vendor: 'mondrian_rest'
11
- format :json
11
+ default_format :json
12
12
 
13
13
  helpers Mondrian::REST::APIHelpers
14
14
  helpers Mondrian::REST::QueryHelper
@@ -36,7 +36,29 @@ module Mondrian::REST
36
36
  end
37
37
  end
38
38
 
39
+ resource :flush do
40
+ params do
41
+ requires :secret, type: String, desc: "Secret key"
42
+ end
43
+ content_type :json, "application/json"
44
+ desc "Flush the schema cache"
45
+
46
+ get do
47
+ if ENV['MONDRIAN_REST_SECRET'].nil?
48
+ error!("Please set MONDRIAN_REST_SECRET to use this endpoint", 403)
49
+ end
50
+ if params[:secret] != ENV['MONDRIAN_REST_SECRET']
51
+ error!("Invalid secret key.", 403)
52
+ end
53
+ {
54
+ 'status' => olap_flush
55
+ }
56
+ end
57
+ end
58
+
39
59
  resource :cubes do
60
+ content_type :json, "application/json"
61
+ default_format :json
40
62
  desc "Returns the cubes defined in this server's schema"
41
63
  get do
42
64
  {
@@ -60,7 +82,7 @@ module Mondrian::REST
60
82
  params do
61
83
  requires :full_name,
62
84
  type: String,
63
- regexp: /[a-z0-9\.,\-\s%\\[\\]\(\)]+/i
85
+ regexp: /[a-z0-9\.,\-\s%\[\]\(\)]+/i
64
86
  end
65
87
  get do
66
88
  member_full_name = URI.decode(params[:full_name])
@@ -81,11 +103,20 @@ module Mondrian::REST
81
103
  resource :aggregate do
82
104
  content_type :xls, "application/vnd.ms-excel"
83
105
  formatter :xls, Mondrian::REST::Formatters::XLS
106
+
84
107
  content_type :csv, "text/csv"
85
108
  formatter :csv, Mondrian::REST::Formatters::CSV
109
+
86
110
  content_type :json, "application/json"
87
111
  formatter :json, Mondrian::REST::Formatters::AggregationJSON
88
112
 
113
+ content_type :jsonrecords, "application/x-jsonrecords"
114
+ formatter :jsonrecords, Mondrian::REST::Formatters::JSONRecords
115
+
116
+ rescue_from PropertyError do |e|
117
+ error!({error: e}, 400)
118
+ end
119
+
89
120
  desc "aggregate from query parameters"
90
121
  params do
91
122
  optional :measures, type: Array
@@ -96,6 +127,7 @@ module Mondrian::REST
96
127
  optional :parents, type: Boolean, desc: "Include members' ancestors"
97
128
  optional :debug, type: Boolean, desc: "Include generated MDX", default: false
98
129
  optional :properties, type: Array, desc: "Include member properties"
130
+ optional :caption, type: Array, desc: "Replace caption with property", default: []
99
131
  end
100
132
  get do
101
133
  cube = get_cube_or_404(params[:cube_name])
@@ -105,6 +137,7 @@ module Mondrian::REST
105
137
  result = mdx(query.to_mdx)
106
138
  result.mdx = mdx_query if params[:debug]
107
139
  result.properties = params[:properties]
140
+ result.caption_properties = params[:caption]
108
141
  result.cube = cube
109
142
 
110
143
  result
@@ -130,7 +163,7 @@ module Mondrian::REST
130
163
  resource :members do
131
164
 
132
165
  params do
133
- optional :member_properties, type: Array
166
+ optional :member_properties, type: Array, default: []
134
167
  end
135
168
 
136
169
  get do
@@ -16,6 +16,7 @@ module Mondrian::REST::Formatters
16
16
  def self.call(result, env)
17
17
  add_parents = env['rack.request.query_hash']['parents'] == 'true'
18
18
  debug = env['rack.request.query_hash']['debug'] == 'true'
19
+ properties = env['rack.request.query_hash']['properties'] || []
19
20
 
20
21
  out = StringIO.new
21
22
  book = WriteExcel.new(out)
@@ -24,7 +25,8 @@ module Mondrian::REST::Formatters
24
25
  Mondrian::REST::Formatters
25
26
  .tidy(result,
26
27
  add_parents: add_parents,
27
- debug: debug)
28
+ debug: debug,
29
+ properties: properties)
28
30
  .each_with_index do |row, i|
29
31
  row.each_with_index { |cell, j|
30
32
  sheet.write(i, j, cell)
@@ -40,10 +42,12 @@ module Mondrian::REST::Formatters
40
42
  def self.call(result, env)
41
43
  add_parents = env['rack.request.query_hash']['parents'] == 'true'
42
44
  debug = env['rack.request.query_hash']['debug'] == 'true'
45
+ properties = env['rack.request.query_hash']['properties'] || []
43
46
 
44
47
  rows = Mondrian::REST::Formatters.tidy(result,
45
48
  add_parents: add_parents,
46
- debug: debug)
49
+ debug: debug,
50
+ properties: properties)
47
51
 
48
52
  ::CSV.generate do |csv|
49
53
  rows.each { |row| csv << row }
@@ -51,6 +55,28 @@ module Mondrian::REST::Formatters
51
55
  end
52
56
  end
53
57
 
58
+ module JSONRecords
59
+ def self.call(result, env)
60
+ add_parents = env['rack.request.query_hash']['parents'] == 'true'
61
+ debug = env['rack.request.query_hash']['debug'] == 'true'
62
+ properties = env['rack.request.query_hash']['properties'] || []
63
+
64
+ rows = Mondrian::REST::Formatters.tidy(result,
65
+ add_parents: add_parents,
66
+ debug: debug,
67
+ properties: properties).lazy
68
+ keys = rows.next
69
+
70
+ {
71
+ data: rows.with_index.with_object([]) { |(row, i), data|
72
+ next if i == 0
73
+ data << Hash[keys.zip(row)]
74
+ }
75
+ }.to_json
76
+
77
+ end
78
+ end
79
+
54
80
  ##
55
81
  # Generate 'tidy data' (http://vita.had.co.nz/papers/tidy-data.pdf)
56
82
  # from a result set.
@@ -58,6 +84,7 @@ module Mondrian::REST::Formatters
58
84
  cube = result.cube
59
85
 
60
86
  add_parents = options[:add_parents]
87
+ properties = options[:properties]
61
88
  rs = result.to_h(add_parents, options[:debug])
62
89
  measures = rs[:axes].first[:members]
63
90
  dimensions = rs[:axis_dimensions][1..-1]
@@ -70,7 +97,7 @@ module Mondrian::REST::Formatters
70
97
  if add_parents
71
98
  hier = cube.dimension(dd[:name])
72
99
  .hierarchies
73
- .first
100
+ .first # TODO: Support other hierarchies
74
101
 
75
102
  level_has_all << hier.has_all?
76
103
  slices << dd[:level_depth]
@@ -86,8 +113,13 @@ module Mondrian::REST::Formatters
86
113
  columns += ["ID #{dd[:level]}", dd[:level]]
87
114
  end
88
115
 
89
- # append measure columns and yield table header
90
- y.yield columns + pluck(measures, :name)
116
+ props = Mondrian::REST::APIHelpers.parse_properties(properties, dimensions)
117
+ pnames = properties.map { |p|
118
+ org.olap4j.mdx.IdentifierNode.parseIdentifier(p).getSegmentList.last.name
119
+ }
120
+
121
+ # append properties and measure columns and yield table header
122
+ y.yield columns + pnames + pluck(measures, :name)
91
123
 
92
124
  prod = rs[:axes][1..-1].map { |e|
93
125
  e[:members].map.with_index { |e_, i| [e_,i] }
@@ -102,24 +134,53 @@ module Mondrian::REST::Formatters
102
134
  msrs = measures.map.with_index { |m, mi|
103
135
  (cidxs + [mi]).reduce(values) { |_, idx| _[idx] }
104
136
  }
137
+
105
138
  if add_parents
106
139
  vdim = cm.each.with_index.reduce([]) { |cnames, (member, j)|
107
- member[:ancestors][0...slices[j] - (level_has_all[j] ? 1 : 0)].each { |ancestor|
140
+ member[:ancestors][0...slices[j] - (level_has_all[j] ? 1 : 0)].reverse.each { |ancestor|
108
141
  cnames += [ancestor[:key], ancestor[:caption]]
109
142
  }
110
143
  cnames += [member[:key], member[:caption]]
111
144
  }
112
- y.yield vdim + msrs
145
+
146
+ y.yield vdim + get_props(cm, pnames, props, dimensions) + msrs
113
147
  else
114
- y.yield pluck(cm, :key)
115
- .zip(pluck(cm, :caption))
116
- .flatten \
117
- + msrs
148
+ row = pluck(cm, :key)
149
+ .zip(pluck(cm, :caption))
150
+ .flatten
151
+
152
+ y.yield row + get_props(cm, pnames, props, dimensions) + msrs
118
153
  end
119
154
  end
120
155
  end
121
156
  end
122
157
 
158
+ def self.get_props(cm, pnames, props, dimensions)
159
+ pvalues = cm.each.with_index.reduce({}) do |h, (member, ax_i)|
160
+ dname = dimensions[ax_i][:name]
161
+ if props[dname] # are there properties requested for members of this dimension?
162
+ mmbr_lvl = dimensions[ax_i][:level]
163
+ (props[dname][mmbr_lvl] || []).each { |p|
164
+ h[p] = member[:properties][p]
165
+ }
166
+ if member[:ancestors]
167
+ props[dname]
168
+ .select { |k, _| k != mmbr_lvl } # levels other than member's own
169
+ .each { |l, p|
170
+ p.each # get all requested props for this level's ancestor
171
+ .with_object(member[:ancestors].find { |anc|
172
+ anc[:level_name] == l
173
+ }) { |prop, anc|
174
+ h[prop] = anc[:properties][prop]
175
+ }
176
+ }
177
+ end
178
+ end
179
+ h
180
+ end # reduce
181
+ pnames.map { |pn| pvalues[pn] }
182
+ end
183
+
123
184
  def self.pluck(a, m)
124
185
  a.map { |e| e[m] }
125
186
  end
@@ -1,4 +1,8 @@
1
1
  module Mondrian::REST
2
+
3
+ class PropertyError < StandardError
4
+ end
5
+
2
6
  module APIHelpers
3
7
 
4
8
  @@olap = nil
@@ -46,5 +50,42 @@ module Mondrian::REST
46
50
  end
47
51
  end
48
52
 
53
+ NEST = Mondrian::REST::Nest.new
54
+ .key { |d| d[0] }
55
+ .key { |d| d[1] }
56
+
57
+ def self.parse_caption_properties(cprops)
58
+ if cprops.nil? or cprops.size < 1
59
+ return {}
60
+ end
61
+
62
+ NEST.map(cprops.map { |cp|
63
+ org.olap4j.mdx.IdentifierNode.parseIdentifier(cp).getSegmentList.to_a.map(&:name)
64
+ })
65
+ end
66
+
67
+ ##
68
+ # parse an array of property specifications like so:
69
+ # input: ['ISICrev4.Level 2.Level 2 ES', 'ISICrev4.Level 1.Level 1 ES']
70
+ # output: {"ISICrev4"=>{"Level 2"=>["Level 2 ES"], "Level 1"=>["Level 1 ES"]}}
71
+ def self.parse_properties(properties, dimensions)
72
+ properties.map { |p|
73
+ sl = org.olap4j.mdx.IdentifierNode.parseIdentifier(p).getSegmentList.to_a
74
+ if sl.size != 3
75
+ raise PropertyError, "Properties must be in the form `Dimension.Level.Property Name`"
76
+ end
77
+
78
+ # check that the dimension is in the drilldown list
79
+ if dimensions.find { |ad| sl[0].name == ad[:name] }.nil?
80
+ raise PropertyError, "Dimension `#{sl[0].name}` not in drilldown list"
81
+ end
82
+
83
+ sl.map(&:name)
84
+ }.group_by(&:first)
85
+ .reduce({}) { |h, (k,v)|
86
+ h[k] = Hash[v.group_by { |x| x[1] }.map { |k1, v1| [k1, v1.map(&:last)] }]
87
+ h
88
+ }
89
+ end
49
90
  end
50
91
  end
@@ -1,12 +1,19 @@
1
+ require 'set'
2
+
1
3
  module Mondrian
2
4
  module OLAP
3
5
 
4
6
  class Cube
7
+
8
+ def named_sets
9
+ raw_cube.getSets
10
+ end
11
+
5
12
  def to_h
6
13
  # gather dimensions
7
14
  dimensions = self.dimensions
8
- .find_all { |d| d.dimension_type != :measures }
9
- .map { |d|
15
+ .find_all { |d| d.dimension_type != :measures }
16
+ .map do |d|
10
17
  {
11
18
  :name => d.name,
12
19
  :caption => d.caption,
@@ -21,18 +28,47 @@ module Mondrian
21
28
  {
22
29
  :name => l.name,
23
30
  :full_name => l.full_name,
24
- :caption => l.caption
31
+ :caption => l.caption,
32
+ :depth => l.depth,
33
+ :annotations => l.annotations,
34
+ :properties => l.own_props.map { |p|
35
+ p.getName
36
+ }
25
37
  }
26
38
  }
27
39
  }
28
40
  }
29
41
  }
30
- }
42
+ end
43
+
44
+ # gather named sets
45
+ named_sets = self.named_sets
46
+ .map do |ns|
47
+
48
+ t = ns.getExpression.getType
49
+ {
50
+ :name => ns.name,
51
+ :dimension => t.getDimension.getName,
52
+ :hierarchy => t.getHierarchy.getName,
53
+ :level => t.getLevel.getName,
54
+ :annotations => begin
55
+ annotated = ns.unwrap(Java::MondrianOlap::Annotated.java_class)
56
+ annotations_hash = annotated.getAnnotationMap.to_hash
57
+ annotations_hash.each do |key, annotation|
58
+ annotations_hash[key] = annotation.getValue
59
+ end
60
+ annotations_hash
61
+ rescue
62
+ {}
63
+ end
64
+ }
65
+ end
31
66
 
32
67
  return {
33
68
  :name => self.name,
34
69
  :annotations => self.annotations,
35
70
  :dimensions => dimensions,
71
+ :named_sets => named_sets,
36
72
  :measures => self.dimensions
37
73
  .find(&:measures?)
38
74
  .hierarchy
@@ -66,6 +102,8 @@ module Mondrian
66
102
  end
67
103
  end
68
104
 
105
+ INTERNAL_PROPS = Set.new(['CATALOG_NAME', 'SCHEMA_NAME', 'CUBE_NAME', 'DIMENSION_UNIQUE_NAME', 'HIERARCHY_UNIQUE_NAME', 'LEVEL_UNIQUE_NAME', 'LEVEL_NUMBER', 'MEMBER_ORDINAL', 'MEMBER_NAME', 'MEMBER_UNIQUE_NAME', 'MEMBER_TYPE', 'MEMBER_GUID', 'MEMBER_CAPTION', 'CHILDREN_CARDINALITY', 'PARENT_LEVEL', 'PARENT_UNIQUE_NAME', 'PARENT_COUNT', 'DESCRIPTION', '$visible', 'MEMBER_KEY', 'IS_PLACEHOLDERMEMBER', 'IS_DATAMEMBER', 'DEPTH', 'DISPLAY_INFO', 'VALUE', '$scenario', 'CELL_FORMATTER', 'CELL_FORMATTER_SCRIPT', 'CELL_FORMATTER_SCRIPT_LANGUAGE', 'DISPLAY_FOLDER', 'FORMAT_EXP', 'KEY', '$name']).freeze
106
+
69
107
  class Level
70
108
  attr_reader :hierarchy
71
109
 
@@ -79,25 +117,39 @@ module Mondrian
79
117
  caption: self.caption,
80
118
  members: self.members
81
119
  .uniq { |m| m.property_value('MEMBER_KEY') }
82
- .map { |m| m.to_h(member_properties) }
120
+ .map { |m| m.to_h(member_properties) },
121
+ :properties => self.own_props.map { |p|
122
+ p.getName
123
+ }
124
+ }
125
+ end
126
+
127
+ def own_props
128
+ @raw_level.properties.select { |p|
129
+ !INTERNAL_PROPS.include?(p.name)
83
130
  }
84
131
  end
132
+
85
133
  end
86
134
 
87
135
  class Member
88
136
 
137
+ alias_method :_caption, :caption
138
+
89
139
  def raw_level
90
140
  @raw_member.getLevel
91
141
  end
92
142
 
93
- def to_h(properties=[])
94
- kv = [:name, :full_name, :caption, :all_member?,
143
+ def to_h(properties=[], caption_property=nil)
144
+ kv = [:name, :full_name, :all_member?,
95
145
  :drillable?, :depth].map { |m|
96
146
  [m, self.send(m)]
97
147
  }
148
+ kv << [:caption, self.pcaption(caption_property)]
98
149
  kv << [:key, self.property_value('MEMBER_KEY')]
99
150
  kv << [:num_children, self.property_value('CHILDREN_CARDINALITY')]
100
151
  kv << [:parent_name, self.property_value('PARENT_UNIQUE_NAME')]
152
+ kv << [:level_name, self.raw_level.name]
101
153
 
102
154
  if properties.size > 0
103
155
  kv << [
@@ -109,6 +161,14 @@ module Mondrian
109
161
  Hash[kv]
110
162
  end
111
163
 
164
+ def pcaption(caption_property=nil)
165
+ if caption_property
166
+ self.property_value(caption_property)
167
+ else
168
+ self._caption
169
+ end
170
+ end
171
+
112
172
  def dimension_info
113
173
  d = @raw_member.getDimension
114
174
  l = @raw_member.getLevel
@@ -130,7 +190,7 @@ module Mondrian
130
190
 
131
191
  class Result
132
192
 
133
- attr_accessor :cube, :mdx, :properties
193
+ attr_accessor :cube, :mdx, :properties, :caption_properties
134
194
 
135
195
  def to_json
136
196
  to_h.to_json
@@ -144,20 +204,31 @@ module Mondrian
144
204
  dimensions = self.axis_members.map { |am| am.first.dimension_info }
145
205
 
146
206
  pprops = unless self.properties.nil?
147
- parse_properties(dimensions[1..-1]) # exclude Measures dimension
207
+ Mondrian::REST::APIHelpers.parse_properties(self.properties,
208
+ dimensions[1..-1]) # exclude Measures dimension
148
209
  else
149
210
  {}
150
211
  end
151
212
 
213
+ cprops = Mondrian::REST::APIHelpers.parse_caption_properties(
214
+ self.caption_properties
215
+ )
216
+
152
217
  rv = {
153
218
  axes: self.axis_members.each_with_index.map { |a, i|
154
219
  {
155
220
  members: a.map { |m|
156
- mh = m.to_h(pprops[m.raw_member.getDimension.name] || [])
221
+ mh = m.to_h(
222
+ pprops.dig(m.raw_member.getDimension.name, m.raw_level.name) || [],
223
+ (cprops.dig(m.raw_member.getDimension.name, m.raw_level.name) || [[]])[0][-1]
224
+ )
157
225
  if parents
158
226
  mh.merge!({
159
227
  ancestors: m.ancestors.map { |ma|
160
- ma.to_h(pprops[ma.raw_member.getDimension.name] || [])
228
+ ma.to_h(
229
+ pprops.dig(ma.raw_member.getDimension.name, ma.raw_level.name) || [],
230
+ (cprops.dig(ma.raw_member.getDimension.name, ma.raw_level.name) || [[]])[0][-1]
231
+ )
161
232
  }
162
233
  })
163
234
  end
@@ -174,28 +245,6 @@ module Mondrian
174
245
  rv
175
246
 
176
247
  end
177
-
178
- private
179
-
180
- ##
181
- #
182
- def parse_properties(dimensions)
183
- self.properties.reduce({}) { |h, p|
184
- sl = org.olap4j.mdx.IdentifierNode.parseIdentifier(p).getSegmentList.to_a
185
- if sl.size != 2
186
- raise "Properties must be in the form `Dimension.Property Name`"
187
- end
188
-
189
- # check that the dimension is in the drilldown list
190
- if dimensions.find { |ad| sl[0].name == ad[:name] }.nil?
191
- raise "Dimension `#{sl[0].name}` not in drilldown list"
192
- end
193
-
194
- h[sl[0].name] ||= []
195
- h[sl[0].name] << sl[1].name
196
- h
197
- }
198
- end
199
248
  end
200
249
  end
201
250
  end
@@ -0,0 +1,121 @@
1
+ # D3 nest operator
2
+ # from: https://gist.github.com/herrstucki/3974701
3
+ module Mondrian::REST
4
+ class Nest
5
+ def initialize
6
+ @nest = {}
7
+ @keys = []
8
+ @sort_keys = []
9
+ end
10
+
11
+ def map(array)
12
+ _map(array, 0)
13
+ end
14
+
15
+ def entries(array)
16
+ _entries(_map(array, 0), 0)
17
+ end
18
+
19
+ def key(&f)
20
+ @keys << f
21
+ self
22
+ end
23
+
24
+ def sort_keys(&order)
25
+ @sort_keys[@keys.size - 1] = order
26
+ self
27
+ end
28
+
29
+ def sort_values(&order)
30
+ @sort_values = order
31
+ self
32
+ end
33
+
34
+ def rollup(&f)
35
+ @rollup = f
36
+ self
37
+ end
38
+
39
+ private
40
+
41
+ def _map(array, depth)
42
+ if depth >= @keys.size
43
+ return @rollup.call(array) if @rollup
44
+ return array.sort { |a, b| @sort_values.call(a, b) } if @sort_values
45
+ return array
46
+ end
47
+
48
+ key = @keys[depth]
49
+ depth += 1
50
+ values_by_key = {}
51
+
52
+ array.each_with_index do |object, i|
53
+ key_value = key.call(object)
54
+ values_by_key[key_value] ||= []
55
+ values_by_key[key_value] << object
56
+ end
57
+
58
+ o = {}
59
+ values_by_key.each do |key_value, values|
60
+ o[key_value] = _map(values, depth)
61
+ end
62
+ o
63
+ end
64
+
65
+ def _entries(map, depth)
66
+ return map if depth >= @keys.size
67
+
68
+ a = []
69
+ sort_key = @sort_keys[depth]
70
+ depth += 1
71
+
72
+ map.each do |key, values|
73
+ a << {
74
+ key: key,
75
+ values: _entries(values, depth)
76
+ }
77
+ end
78
+
79
+ if sort_key
80
+ a.sort { |a, b| sort_key.call(a[:key], b[:key]) }
81
+ else
82
+ a
83
+ end
84
+ end
85
+ end
86
+
87
+ end
88
+
89
+
90
+ # TEST
91
+
92
+ # require 'pp'
93
+
94
+ # data = [
95
+ # {year: "2013", category: "Cars", value: 1000},
96
+ # {year: "2011", category: "Cars", value: 1200},
97
+ # {year: "2012", category: "Cars", value: 1300},
98
+ # {year: "2011", category: "Planes", value: 1000},
99
+ # {year: "2011", category: "Planes", value: 1100},
100
+ # {year: "2012", category: "Planes", value: 1200},
101
+ # {year: "2011", category: "Planes", value: 1300},
102
+ # {year: "2012", category: "Bikes", value: 1000},
103
+ # {year: "2011", category: "Bikes", value: 1100},
104
+ # {year: "2011", category: "Bikes", value: 1200},
105
+ # ]
106
+
107
+ # nest = Nest.new
108
+ # nest.key { |d| d[:year] }
109
+ # nest.sort_keys { |a,b| a <=> b }
110
+ # nest.key { |d| d[:category] }
111
+ # # nest.sort_values { |a,b| b[:value] <=> a[:value] }
112
+ # nest.rollup do |values|
113
+ # {
114
+ # count: values.size,
115
+ # total_value: values.map { |d| d[:value] }.reduce(:+),
116
+ # raw: values
117
+ # }
118
+ # end
119
+
120
+ # # pp nest.entries(data)
121
+ # pp nest.map(data)
@@ -1,3 +1,4 @@
1
+ # coding: utf-8
1
2
  module Mondrian::REST
2
3
  module QueryHelper
3
4
 
@@ -18,18 +19,15 @@ module Mondrian::REST
18
19
 
19
20
  def get_member(cube, member_exp)
20
21
  begin
21
- rm = cube.raw_cube
22
- .lookupMember(org.olap4j.mdx.IdentifierNode.parseIdentifier(member_exp).getSegmentList)
22
+ return cube.member(member_exp)
23
23
  rescue Java::JavaLang::IllegalArgumentException
24
24
  error!("Illegal member expression: #{member_exp}", 400)
25
25
  end
26
- member = nil
27
- unless rm.nil?
28
- member = Mondrian::OLAP::Member.new(rm)
29
- end
30
- member
31
26
  end
32
27
 
28
+ ##
29
+ # Parses a string containing a 'cut' expression
30
+ # It can be a set (`{Dim.Mem, Dim2.Mem2}`), a range (`([Time].[Year].[1997]:[Time].[Year].[1998])`) or a member identifier (`[Time].[Year].[1998]`).
33
31
  def parse_cut(cube, cut_expr)
34
32
  p = mdx_parser.parseExpression(cut_expr)
35
33
 
@@ -38,13 +36,14 @@ module Mondrian::REST
38
36
  case p.getOperatorName
39
37
  when "{}"
40
38
  # check that the set contains only Members of a single dimension level
41
- ls = p.getArgList.map { |id_node|
42
- get_member(cube, unparse_node(id_node)).raw_level
43
- }.uniq
39
+ set_members = p.getArgList.map { |id_node|
40
+ get_member(cube, unparse_node(id_node))
41
+ }
42
+ ls = set_members.map(&:raw_level).uniq
44
43
  unless ls.size == 1
45
44
  error!("Illegal cut: " + cut_expr, 400)
46
45
  end
47
- { level: ls.first, cut: unparse_node(p), type: :set }
46
+ { level: ls.first, cut: unparse_node(p), type: :set, set_members: set_members }
48
47
  when "()"
49
48
  # check that the range contains a valid range
50
49
 
@@ -69,6 +68,11 @@ module Mondrian::REST
69
68
  # if `cut_expr` looks like a member, check that it's level is
70
69
  # equal to `level`
71
70
  m = get_member(cube, cut_expr)
71
+
72
+ if m.nil?
73
+ error!("Illegal cut: #{cut_expr} — Member does not exist", 400)
74
+ end
75
+
72
76
  { level: m.raw_level, cut: cut_expr, type: :member }
73
77
  else
74
78
  error!("Illegal cut: " + cut_expr, 400)
@@ -79,6 +83,13 @@ module Mondrian::REST
79
83
  # Parses a drilldown specification
80
84
  # XXX TODO write doc
81
85
  def parse_drilldown(cube, drilldown)
86
+
87
+ # check if the drilldown is a named set
88
+ named_sets = cube.named_sets
89
+ if ns = named_sets.find { |ns| ns.name == drilldown }
90
+ return ns
91
+ end
92
+
82
93
  begin
83
94
  s = org.olap4j.mdx.IdentifierNode.parseIdentifier(drilldown).getSegmentList
84
95
  rescue Java::JavaLang::IllegalArgumentException
@@ -148,8 +159,11 @@ module Mondrian::REST
148
159
  }
149
160
 
150
161
  dd = query_axes.map do |qa|
162
+ # if drilling down on a named set
163
+ if qa.kind_of?(Java::MondrianOlap4j::MondrianOlap4jNamedSet)
164
+ "[#{qa.name}]"
151
165
  # there's a slice (cut) on this axis
152
- if slicer_axis[qa.raw_level]
166
+ elsif slicer_axis[qa.raw_level]
153
167
  cut = slicer_axis.delete(qa.raw_level)
154
168
  case cut[:type]
155
169
  when :member
@@ -157,6 +171,24 @@ module Mondrian::REST
157
171
  else
158
172
  cut[:cut]
159
173
  end
174
+ elsif cut = slicer_axis.find { |lvl, cut|
175
+ qa.raw_level.hierarchy == lvl.hierarchy && lvl.depth < qa.depth
176
+ }
177
+ slicer_axis.delete(cut[0])
178
+ cut = cut[1]
179
+
180
+ case cut[:type]
181
+ when :member
182
+ "DESCENDANTS(#{cut[:cut]}, #{qa.full_name})"
183
+ when :set
184
+ # TODO
185
+ "{" + cut[:set_members].map { |m|
186
+ "DESCENDANTS(#{m.full_name}, #{qa.full_name})"
187
+ }.join(",") + "}"
188
+ when :range
189
+ # TODO
190
+ raise "Unsupported operation"
191
+ end
160
192
  else
161
193
  qa.raw_level.unique_name + '.Members'
162
194
  end
@@ -180,7 +212,7 @@ module Mondrian::REST
180
212
 
181
213
  # slicer axes (cut)
182
214
  if slicer_axis.size >= 1
183
- query = query.where(slicer_axis.values.map { |v| v[:cut] })
215
+ query = query.where(slicer_axis.values.map { |v| v[:cut] }.join(' * '))
184
216
  end
185
217
  query
186
218
  end
data/lib/mondrian_rest.rb CHANGED
@@ -5,5 +5,6 @@ Java::JavaLang::System.setProperty("jdbc.driver.autoload", "true")
5
5
  require 'mondrian-olap'
6
6
  require 'grape'
7
7
 
8
+ require_relative './mondrian_rest/nest.rb'
8
9
  require_relative './mondrian_rest/api.rb'
9
10
  require_relative './mondrian_rest/mondrian_ext.rb'
@@ -1,7 +1,7 @@
1
1
  # coding: utf-8
2
2
  Gem::Specification.new do |s|
3
3
  s.name = "mondrian-rest"
4
- s.version = '0.5.0'
4
+ s.version = '0.6.0'
5
5
  s.authors = ["Manuel Aristarán"]
6
6
  s.email = ["manuel@jazzido.com"]
7
7
  s.homepage = "https://github.com/jazzido/mondrian-rest"
@@ -14,14 +14,14 @@ Gem::Specification.new do |s|
14
14
  s.files = `git ls-files`.split("\n").reject { |f| f =~ /^spec\// }
15
15
  s.require_paths = ["lib"]
16
16
 
17
- s.add_runtime_dependency 'mondrian-olap', ["~> 0.7.0"]
18
- s.add_runtime_dependency 'grape', ["~> 0.14.0"]
17
+ s.add_runtime_dependency 'mondrian-olap', ["~> 0.8.0"]
18
+ s.add_runtime_dependency 'grape', ["~> 0.19.0"]
19
19
  s.add_runtime_dependency 'writeexcel', '~> 1.0', '>= 1.0.5'
20
20
 
21
- s.add_development_dependency 'rake', '~> 10.4', '>= 10.4.2'
22
- s.add_development_dependency 'rspec', '~> 3.4', '>= 3.4.0'
23
- s.add_development_dependency 'jdbc-derby', '~> 10.11', '>= 10.11.1.1'
24
- s.add_development_dependency 'jdbc-sqlite3', '~> 3.8', '>= 3.8.11.2'
25
- s.add_development_dependency 'rack-test', '~> 0.6.3'
26
- s.add_development_dependency 'rubyzip', '~> 1.1', '>= 1.1.7'
21
+ s.add_development_dependency 'rake'
22
+ s.add_development_dependency 'rspec'
23
+ s.add_development_dependency 'jdbc-derby'
24
+ s.add_development_dependency 'jdbc-sqlite3'
25
+ s.add_development_dependency 'rack-test'
26
+ s.add_development_dependency 'rubyzip'
27
27
  end
metadata CHANGED
@@ -1,21 +1,21 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mondrian-rest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: java
6
6
  authors:
7
7
  - Manuel Aristarán
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-10-25 00:00:00.000000000 Z
11
+ date: 2017-05-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 0.7.0
18
+ version: 0.8.0
19
19
  name: mondrian-olap
20
20
  prerelease: false
21
21
  type: :runtime
@@ -23,13 +23,13 @@ dependencies:
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.7.0
26
+ version: 0.8.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: 0.14.0
32
+ version: 0.19.0
33
33
  name: grape
34
34
  prerelease: false
35
35
  type: :runtime
@@ -37,7 +37,7 @@ dependencies:
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 0.14.0
40
+ version: 0.19.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
@@ -61,117 +61,87 @@ dependencies:
61
61
  - !ruby/object:Gem::Dependency
62
62
  requirement: !ruby/object:Gem::Requirement
63
63
  requirements:
64
- - - "~>"
65
- - !ruby/object:Gem::Version
66
- version: '10.4'
67
64
  - - ">="
68
65
  - !ruby/object:Gem::Version
69
- version: 10.4.2
66
+ version: '0'
70
67
  name: rake
71
68
  prerelease: false
72
69
  type: :development
73
70
  version_requirements: !ruby/object:Gem::Requirement
74
71
  requirements:
75
- - - "~>"
76
- - !ruby/object:Gem::Version
77
- version: '10.4'
78
72
  - - ">="
79
73
  - !ruby/object:Gem::Version
80
- version: 10.4.2
74
+ version: '0'
81
75
  - !ruby/object:Gem::Dependency
82
76
  requirement: !ruby/object:Gem::Requirement
83
77
  requirements:
84
- - - "~>"
85
- - !ruby/object:Gem::Version
86
- version: '3.4'
87
78
  - - ">="
88
79
  - !ruby/object:Gem::Version
89
- version: 3.4.0
80
+ version: '0'
90
81
  name: rspec
91
82
  prerelease: false
92
83
  type: :development
93
84
  version_requirements: !ruby/object:Gem::Requirement
94
85
  requirements:
95
- - - "~>"
96
- - !ruby/object:Gem::Version
97
- version: '3.4'
98
86
  - - ">="
99
87
  - !ruby/object:Gem::Version
100
- version: 3.4.0
88
+ version: '0'
101
89
  - !ruby/object:Gem::Dependency
102
90
  requirement: !ruby/object:Gem::Requirement
103
91
  requirements:
104
- - - "~>"
105
- - !ruby/object:Gem::Version
106
- version: '10.11'
107
92
  - - ">="
108
93
  - !ruby/object:Gem::Version
109
- version: 10.11.1.1
94
+ version: '0'
110
95
  name: jdbc-derby
111
96
  prerelease: false
112
97
  type: :development
113
98
  version_requirements: !ruby/object:Gem::Requirement
114
99
  requirements:
115
- - - "~>"
116
- - !ruby/object:Gem::Version
117
- version: '10.11'
118
100
  - - ">="
119
101
  - !ruby/object:Gem::Version
120
- version: 10.11.1.1
102
+ version: '0'
121
103
  - !ruby/object:Gem::Dependency
122
104
  requirement: !ruby/object:Gem::Requirement
123
105
  requirements:
124
- - - "~>"
125
- - !ruby/object:Gem::Version
126
- version: '3.8'
127
106
  - - ">="
128
107
  - !ruby/object:Gem::Version
129
- version: 3.8.11.2
108
+ version: '0'
130
109
  name: jdbc-sqlite3
131
110
  prerelease: false
132
111
  type: :development
133
112
  version_requirements: !ruby/object:Gem::Requirement
134
113
  requirements:
135
- - - "~>"
136
- - !ruby/object:Gem::Version
137
- version: '3.8'
138
114
  - - ">="
139
115
  - !ruby/object:Gem::Version
140
- version: 3.8.11.2
116
+ version: '0'
141
117
  - !ruby/object:Gem::Dependency
142
118
  requirement: !ruby/object:Gem::Requirement
143
119
  requirements:
144
- - - "~>"
120
+ - - ">="
145
121
  - !ruby/object:Gem::Version
146
- version: 0.6.3
122
+ version: '0'
147
123
  name: rack-test
148
124
  prerelease: false
149
125
  type: :development
150
126
  version_requirements: !ruby/object:Gem::Requirement
151
127
  requirements:
152
- - - "~>"
128
+ - - ">="
153
129
  - !ruby/object:Gem::Version
154
- version: 0.6.3
130
+ version: '0'
155
131
  - !ruby/object:Gem::Dependency
156
132
  requirement: !ruby/object:Gem::Requirement
157
133
  requirements:
158
- - - "~>"
159
- - !ruby/object:Gem::Version
160
- version: '1.1'
161
134
  - - ">="
162
135
  - !ruby/object:Gem::Version
163
- version: 1.1.7
136
+ version: '0'
164
137
  name: rubyzip
165
138
  prerelease: false
166
139
  type: :development
167
140
  version_requirements: !ruby/object:Gem::Requirement
168
141
  requirements:
169
- - - "~>"
170
- - !ruby/object:Gem::Version
171
- version: '1.1'
172
142
  - - ">="
173
143
  - !ruby/object:Gem::Version
174
- version: 1.1.7
144
+ version: '0'
175
145
  description: A REST interface for Mondrian ROLAP server
176
146
  email:
177
147
  - manuel@jazzido.com
@@ -189,6 +159,7 @@ files:
189
159
  - lib/mondrian_rest/api_formatters.rb
190
160
  - lib/mondrian_rest/api_helpers.rb
191
161
  - lib/mondrian_rest/mondrian_ext.rb
162
+ - lib/mondrian_rest/nest.rb
192
163
  - lib/mondrian_rest/query_helper.rb
193
164
  - lib/mondrian_rest/version.rb
194
165
  - mondrian-rest.gemspec
@@ -212,7 +183,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
212
183
  version: '0'
213
184
  requirements: []
214
185
  rubyforge_project:
215
- rubygems_version: 2.4.8
186
+ rubygems_version: 2.6.8
216
187
  signing_key:
217
188
  specification_version: 4
218
189
  summary: A REST interface for Mondrian ROLAP server