rubiks 0.0.4 → 0.0.5
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/.gitignore +30 -15
- data/.rvmrc +1 -0
- data/.travis.yml +3 -4
- data/.yardopts +6 -0
- data/CHANGELOG.md +21 -0
- data/CONTRIBUTIG.md +46 -0
- data/Gemfile +4 -1
- data/{LICENSE.txt → LICENSE.md} +1 -2
- data/README.md +97 -4
- data/Rakefile +8 -1
- data/examples/mondrian_docs.yml +36 -0
- data/lib/rubiks/examples.rb +61 -0
- data/lib/rubiks/nodes/annotated_node.rb +6 -0
- data/lib/rubiks/nodes/calculated_member.rb +81 -0
- data/lib/rubiks/nodes/cube.rb +74 -2
- data/lib/rubiks/nodes/dimension.rb +18 -0
- data/lib/rubiks/nodes/hierarchy.rb +47 -2
- data/lib/rubiks/nodes/level.rb +106 -2
- data/lib/rubiks/nodes/measure.rb +23 -14
- data/lib/rubiks/nodes/schema.rb +19 -8
- data/lib/rubiks/nodes/validated_node.rb +33 -1
- data/lib/rubiks/version.rb +1 -1
- data/rubiks.gemspec +4 -2
- data/spec/examples/mondrian_docs_example_spec.rb +108 -0
- data/spec/examples/simple_mondrian_example_spec.rb +84 -0
- data/spec/rubiks/nodes/calculated_member_spec.rb +50 -0
- data/spec/rubiks/nodes/cube_spec.rb +32 -8
- data/spec/rubiks/nodes/dimension_spec.rb +15 -0
- data/spec/rubiks/nodes/hierarchy_spec.rb +22 -1
- data/spec/rubiks/nodes/level_spec.rb +68 -1
- data/spec/rubiks/nodes/measure_spec.rb +29 -5
- data/spec/rubiks/nodes/schema_spec.rb +2 -23
- data/spec/spec_helper.rb +30 -11
- data/spec/support/schema_context.rb +17 -4
- metadata +26 -10
- data/Guardfile +0 -5
- data/spec/examples/mondrian_xml_example_spec.rb +0 -91
@@ -4,15 +4,33 @@ require 'rubiks/nodes/level'
|
|
4
4
|
module ::Rubiks
|
5
5
|
|
6
6
|
class Hierarchy < ::Rubiks::AnnotatedNode
|
7
|
+
value :dimension, String
|
8
|
+
value :table, String
|
7
9
|
child :levels, [::Rubiks::Level]
|
8
10
|
|
9
|
-
validates :levels_present
|
11
|
+
validates :levels_present, :dimension_present
|
10
12
|
|
11
13
|
def self.new_from_hash(hash={})
|
12
|
-
new_instance = new('',[])
|
14
|
+
new_instance = new('','','',[])
|
13
15
|
return new_instance.from_hash(hash)
|
14
16
|
end
|
15
17
|
|
18
|
+
def dimension_present
|
19
|
+
errors << 'Dimension required on Hierarchy' if self.dimension.blank?
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse_table(input_value)
|
23
|
+
return if input_value.nil?
|
24
|
+
|
25
|
+
self.table = input_value.to_s
|
26
|
+
end
|
27
|
+
|
28
|
+
def parse_dimension(dimension_value)
|
29
|
+
return if dimension_value.nil?
|
30
|
+
|
31
|
+
self.dimension = dimension_value.to_s
|
32
|
+
end
|
33
|
+
|
16
34
|
def levels_present
|
17
35
|
if self.levels.present?
|
18
36
|
self.levels.each do |level|
|
@@ -29,6 +47,8 @@ module ::Rubiks
|
|
29
47
|
working_hash.stringify_keys!
|
30
48
|
|
31
49
|
parse_name(working_hash.delete('name'))
|
50
|
+
parse_table(working_hash.delete('table'))
|
51
|
+
parse_dimension(working_hash.delete('dimension'))
|
32
52
|
parse_levels(working_hash.delete('levels'))
|
33
53
|
return self
|
34
54
|
end
|
@@ -45,11 +65,36 @@ module ::Rubiks
|
|
45
65
|
hash = {}
|
46
66
|
|
47
67
|
hash['name'] = self.name.to_s if self.name.present?
|
68
|
+
hash['table'] = self.table.to_s if self.table.present?
|
69
|
+
hash['dimension'] = self.dimension.to_s if self.dimension.present?
|
48
70
|
hash['levels'] = self.levels.map(&:to_hash) if self.levels.present?
|
49
71
|
|
50
72
|
return hash
|
51
73
|
end
|
52
74
|
|
75
|
+
def to_xml(builder = nil)
|
76
|
+
builder = Builder::XmlMarkup.new(:indent => 2) if builder.nil?
|
77
|
+
|
78
|
+
attrs = Hash.new
|
79
|
+
attrs['name'] = self.name.titleize if self.name.present?
|
80
|
+
attrs['primaryKey'] = 'id'
|
81
|
+
attrs['hasAll'] = 'true'
|
82
|
+
|
83
|
+
builder.hierarchy(attrs) {
|
84
|
+
table_attrs = Hash.new
|
85
|
+
if self.table.present?
|
86
|
+
table_attrs['name'] = self.table
|
87
|
+
elsif self.dimension.present?
|
88
|
+
table_attrs['name'] = "view_#{dimension.tableize}"
|
89
|
+
end
|
90
|
+
|
91
|
+
builder.table(table_attrs)
|
92
|
+
|
93
|
+
self.levels.each do |level|
|
94
|
+
level.to_xml(builder)
|
95
|
+
end if self.levels.present?
|
96
|
+
}
|
97
|
+
end
|
53
98
|
end
|
54
99
|
|
55
100
|
end
|
data/lib/rubiks/nodes/level.rb
CHANGED
@@ -1,9 +1,20 @@
|
|
1
1
|
require 'rubiks/nodes/annotated_node'
|
2
|
-
require 'rubiks/nodes/hierarchy'
|
3
2
|
|
4
3
|
module ::Rubiks
|
5
4
|
|
6
5
|
class Level < ::Rubiks::AnnotatedNode
|
6
|
+
CARDINALITIES = %w[ low normal high ]
|
7
|
+
DATA_TYPES = %w[ String Integer Numeric Boolean Date Time Timestamp ]
|
8
|
+
|
9
|
+
value :cardinality, String
|
10
|
+
value :contiguous_value, Fixnum
|
11
|
+
value :sort_column, String
|
12
|
+
value :data_type, String
|
13
|
+
value :column, String
|
14
|
+
value :name_column, String
|
15
|
+
|
16
|
+
validates :cardinality_if_present, :data_type_if_present
|
17
|
+
|
7
18
|
def self.new_from_hash(hash={})
|
8
19
|
new_instance = new
|
9
20
|
return new_instance.from_hash(hash)
|
@@ -14,16 +25,109 @@ module ::Rubiks
|
|
14
25
|
working_hash.stringify_keys!
|
15
26
|
|
16
27
|
parse_name(working_hash.delete('name'))
|
28
|
+
parse_name_column(working_hash.delete('name_column'))
|
29
|
+
parse_column(working_hash.delete('column'))
|
30
|
+
parse_data_type(working_hash.delete('data_type'))
|
31
|
+
parse_contiguous_value(working_hash.delete('contiguous'))
|
32
|
+
parse_cardinality(working_hash.delete('cardinality'))
|
33
|
+
parse_sort_column(working_hash.delete('sort'))
|
34
|
+
parse_sort_column(working_hash.delete('sorted'))
|
35
|
+
parse_sort_column(working_hash.delete('sort_column'))
|
17
36
|
return self
|
18
37
|
end
|
19
38
|
|
39
|
+
def parse_column(input_value)
|
40
|
+
return if input_value.nil?
|
41
|
+
|
42
|
+
self.column = input_value.to_s
|
43
|
+
end
|
44
|
+
|
45
|
+
def parse_name_column(input_value)
|
46
|
+
return if input_value.nil?
|
47
|
+
|
48
|
+
self.name_column = input_value.to_s
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse_contiguous_value(input_value)
|
52
|
+
return if input_value.nil?
|
53
|
+
|
54
|
+
self.contiguous_value = !!input_value ? 1 : 0
|
55
|
+
end
|
56
|
+
|
57
|
+
def parse_sort_column(sort_column_value)
|
58
|
+
return if sort_column_value.nil?
|
59
|
+
|
60
|
+
if sort_column_value.kind_of?(::TrueClass)
|
61
|
+
self.sort_column = "#{self.name}_sort"
|
62
|
+
|
63
|
+
elsif sort_column_value.kind_of?(::String)
|
64
|
+
self.sort_column = sort_column_value
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def data_type_if_present
|
69
|
+
if self.data_type.present? && !::Rubiks::Level::DATA_TYPES.include?(self.data_type)
|
70
|
+
errors << "DataType '#{self.data_type}' must be one of #{::Rubiks::Level::DATA_TYPES.join(', ')} on Level"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def parse_data_type(data_type_value)
|
75
|
+
return if data_type_value.nil?
|
76
|
+
|
77
|
+
self.data_type = data_type_value.to_s.capitalize
|
78
|
+
end
|
79
|
+
|
80
|
+
def cardinality_if_present
|
81
|
+
if self.cardinality.present? && !::Rubiks::Level::CARDINALITIES.include?(self.cardinality)
|
82
|
+
errors << "Cardinality '#{self.cardinality}' must be one of #{::Rubiks::Level::CARDINALITIES.join(', ')} on Level"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def parse_cardinality(cardinality_value)
|
87
|
+
return if cardinality_value.nil?
|
88
|
+
|
89
|
+
self.cardinality = cardinality_value.to_s
|
90
|
+
end
|
91
|
+
|
20
92
|
def to_hash
|
21
93
|
hash = {}
|
22
94
|
|
23
|
-
|
95
|
+
if self.name.present?
|
96
|
+
hash['name'] = self.name.to_s
|
97
|
+
hash['display_name'] = self.display_name
|
98
|
+
end
|
99
|
+
hash['contiguous'] = true if self.contiguous_value.present? && self.contiguous_value == 1
|
100
|
+
hash['cardinality'] = self.cardinality if self.cardinality.present?
|
101
|
+
hash['data_type'] = self.data_type if self.data_type.present?
|
102
|
+
|
103
|
+
if self.column.present?
|
104
|
+
hash['column'] = self.column
|
105
|
+
elsif self.name.present?
|
106
|
+
hash['column'] = self.name
|
107
|
+
end
|
24
108
|
|
25
109
|
return hash
|
26
110
|
end
|
111
|
+
|
112
|
+
def to_xml(builder = nil)
|
113
|
+
builder = Builder::XmlMarkup.new(:indent => 2) if builder.nil?
|
114
|
+
|
115
|
+
attrs = {}
|
116
|
+
|
117
|
+
if self.name.present?
|
118
|
+
attrs['name'] = self.display_name
|
119
|
+
attrs['column'] = self.column || self.name
|
120
|
+
end
|
121
|
+
attrs['type'] = self.data_type if self.data_type.present?
|
122
|
+
attrs['ordinalColumn'] = self.sort_column if self.sort_column.present?
|
123
|
+
attrs['nameColumn'] = self.name_column if self.name_column.present?
|
124
|
+
|
125
|
+
builder.level(attrs)
|
126
|
+
end
|
127
|
+
|
128
|
+
def to_json
|
129
|
+
MultiJson.dump(self.to_hash)
|
130
|
+
end
|
27
131
|
end
|
28
132
|
|
29
133
|
end
|
data/lib/rubiks/nodes/measure.rb
CHANGED
@@ -3,11 +3,11 @@ require 'rubiks/nodes/validated_node'
|
|
3
3
|
module ::Rubiks
|
4
4
|
|
5
5
|
class Measure < ::Rubiks::AnnotatedNode
|
6
|
-
value :column, String
|
7
6
|
value :aggregator, String
|
7
|
+
value :column, String
|
8
8
|
value :format_string, String
|
9
9
|
|
10
|
-
validates :
|
10
|
+
validates :aggregator_present
|
11
11
|
|
12
12
|
def self.new_from_hash(hash={})
|
13
13
|
new_instance = new
|
@@ -28,26 +28,24 @@ module ::Rubiks
|
|
28
28
|
def to_hash
|
29
29
|
hash = {}
|
30
30
|
|
31
|
-
hash['name'] = self.name
|
32
|
-
hash['
|
33
|
-
hash['
|
34
|
-
hash['
|
31
|
+
hash['name'] = self.name if self.name.present?
|
32
|
+
hash['aggregator'] = self.aggregator if self.aggregator.present?
|
33
|
+
hash['format_string'] = self.format_string if self.format_string.present?
|
34
|
+
hash['column'] = self.column if self.column.present?
|
35
35
|
|
36
36
|
return hash
|
37
37
|
end
|
38
38
|
|
39
|
-
def
|
40
|
-
errors << '
|
39
|
+
def aggregator_present
|
40
|
+
errors << 'Aggregator required on Measure' if self.aggregator.blank?
|
41
41
|
end
|
42
42
|
|
43
43
|
def parse_column(column_value)
|
44
|
-
return if column_value.nil?
|
44
|
+
return if column_value.nil? && self.name.blank?
|
45
45
|
|
46
|
-
self.column = column_value.
|
47
|
-
|
48
|
-
|
49
|
-
def aggregator_present
|
50
|
-
errors << 'Aggregator required on Measure' if self.aggregator.blank?
|
46
|
+
self.column = column_value.nil? ?
|
47
|
+
self.name.underscore :
|
48
|
+
column_value.to_s
|
51
49
|
end
|
52
50
|
|
53
51
|
def parse_aggregator(aggregator_value)
|
@@ -61,6 +59,17 @@ module ::Rubiks
|
|
61
59
|
|
62
60
|
self.format_string = format_string_value.to_s
|
63
61
|
end
|
62
|
+
|
63
|
+
def to_xml(builder = nil)
|
64
|
+
builder = Builder::XmlMarkup.new(:indent => 2) if builder.nil?
|
65
|
+
|
66
|
+
attrs = self.to_hash
|
67
|
+
attrs['name'] = self.display_name if self.name.present?
|
68
|
+
attrs.keys.each do |key|
|
69
|
+
attrs[key.camelize(:lower)] = attrs.delete(key)
|
70
|
+
end
|
71
|
+
builder.measure(attrs)
|
72
|
+
end
|
64
73
|
end
|
65
74
|
|
66
75
|
end
|
data/lib/rubiks/nodes/schema.rb
CHANGED
@@ -1,16 +1,17 @@
|
|
1
|
-
require 'rubiks/nodes/
|
1
|
+
require 'rubiks/nodes/annotated_node'
|
2
2
|
require 'rubiks/nodes/cube'
|
3
3
|
require 'multi_json'
|
4
|
+
require 'builder'
|
4
5
|
|
5
6
|
module ::Rubiks
|
6
7
|
|
7
|
-
class Schema < ::Rubiks::
|
8
|
+
class Schema < ::Rubiks::AnnotatedNode
|
8
9
|
child :cubes, [::Rubiks::Cube]
|
9
10
|
|
10
11
|
validates :cubes_present
|
11
12
|
|
12
13
|
def self.new_from_hash(hash={})
|
13
|
-
new_instance = new([])
|
14
|
+
new_instance = new('',[])
|
14
15
|
return new_instance.from_hash(hash)
|
15
16
|
end
|
16
17
|
|
@@ -29,6 +30,8 @@ module ::Rubiks
|
|
29
30
|
return self if working_hash.nil?
|
30
31
|
working_hash.stringify_keys!
|
31
32
|
|
33
|
+
parse_name(working_hash.delete('name'))
|
34
|
+
self.name = 'default' if self.name.blank?
|
32
35
|
parse_cubes(working_hash.delete('cubes'))
|
33
36
|
return self
|
34
37
|
end
|
@@ -44,17 +47,25 @@ module ::Rubiks
|
|
44
47
|
def to_hash
|
45
48
|
hash = {}
|
46
49
|
|
50
|
+
hash['name'] = self.name if self.name.present?
|
47
51
|
hash['cubes'] = self.cubes.map(&:to_hash) if self.cubes.present?
|
48
52
|
|
49
53
|
return hash
|
50
54
|
end
|
51
55
|
|
52
|
-
def
|
53
|
-
|
54
|
-
|
56
|
+
def to_xml(builder = nil)
|
57
|
+
builder = Builder::XmlMarkup.new(:indent => 2) if builder.nil?
|
58
|
+
|
59
|
+
builder.instruct!
|
60
|
+
|
61
|
+
attrs = self.to_hash
|
62
|
+
attrs.delete('cubes')
|
55
63
|
|
56
|
-
|
57
|
-
|
64
|
+
builder.schema(attrs) {
|
65
|
+
self.cubes.each do |cube|
|
66
|
+
cube.to_xml(builder)
|
67
|
+
end
|
68
|
+
}
|
58
69
|
end
|
59
70
|
end
|
60
71
|
|
@@ -2,6 +2,38 @@ require 'rltk'
|
|
2
2
|
require 'rltk/ast'
|
3
3
|
|
4
4
|
module ::Rubiks
|
5
|
+
|
6
|
+
# == Rubiks ValidatedNode
|
7
|
+
#
|
8
|
+
# Provides a basic validation framework to ASTNodes.
|
9
|
+
#
|
10
|
+
# A minimal implementation could be:
|
11
|
+
#
|
12
|
+
# class NamedNode < ::Rubiks::ValidatedNode
|
13
|
+
# name :name, String
|
14
|
+
#
|
15
|
+
# validates :name_present
|
16
|
+
#
|
17
|
+
# def name_present
|
18
|
+
# errors << "Name required on NamedNode" if self.name.blank?
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# def parse_name(name_value)
|
22
|
+
# return if name_value.nil?
|
23
|
+
#
|
24
|
+
# self.name = name_value.to_s
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# Which provides you with the following behavior:
|
29
|
+
#
|
30
|
+
# name_node = nameNode.new
|
31
|
+
# name_node.valid? # => false
|
32
|
+
# name_node.errors # => ["Name required on NamedNode"]
|
33
|
+
#
|
34
|
+
# name_node.name = 7
|
35
|
+
# name_node.valid? # => true
|
36
|
+
#
|
5
37
|
class ValidatedNode < ::RLTK::ASTNode
|
6
38
|
|
7
39
|
class << self
|
@@ -44,4 +76,4 @@ module ::Rubiks
|
|
44
76
|
end
|
45
77
|
|
46
78
|
end
|
47
|
-
end
|
79
|
+
end
|
data/lib/rubiks/version.rb
CHANGED
data/rubiks.gemspec
CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |gem|
|
|
8
8
|
gem.version = Rubiks::VERSION
|
9
9
|
gem.authors = ['JohnnyT']
|
10
10
|
gem.email = ['johnnyt@moneydesktop.com']
|
11
|
-
gem.
|
12
|
-
gem.
|
11
|
+
gem.summary = %q{A gem to provide translation of OLAP schemas}
|
12
|
+
gem.description = %q{A gem to allow defining an OLAP schema from a hash and generate an XML schema for Mondrian}
|
13
13
|
gem.homepage = 'https://github.com/moneydesktop/rubiks'
|
14
14
|
|
15
15
|
gem.files = `git ls-files`.split($/)
|
@@ -20,4 +20,6 @@ Gem::Specification.new do |gem|
|
|
20
20
|
gem.add_dependency 'rltk'
|
21
21
|
gem.add_dependency 'activesupport'
|
22
22
|
gem.add_dependency 'builder'
|
23
|
+
|
24
|
+
# Check the Gemfile for test and development dependencies
|
23
25
|
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
# This example is taken from the Mondrian documentation:
|
4
|
+
# http://mondrian.pentaho.com/documentation/schema.php#Cubes_and_dimensions
|
5
|
+
# then modified to use Rails/Rubiks conventions
|
6
|
+
#
|
7
|
+
# We want the output to be:
|
8
|
+
#
|
9
|
+
# <schema name="default">
|
10
|
+
# <cube name="Sales">
|
11
|
+
# <table name="view_sales"/>
|
12
|
+
#
|
13
|
+
# <dimension name="Date" foreignKey="date_id">
|
14
|
+
# <hierarchy name="Year Quarter Month" primaryKey="id" hasAll="true">
|
15
|
+
# <table name="view_dates"/>
|
16
|
+
# <level name="Year" column="year"/>
|
17
|
+
# <level name="Quarter" column="quarter"/>
|
18
|
+
# <level name="Month" column="month"/>
|
19
|
+
# </hierarchy>
|
20
|
+
# </dimension>
|
21
|
+
#
|
22
|
+
# <measure name="Unit Sales" column="unit_sales" aggregator="sum" formatString="#,###"/>
|
23
|
+
#
|
24
|
+
# <calculatedMember name="Profit" dimension="Measures" formula="[Measures].[Store Sales] - [Measures].[Store Cost]">
|
25
|
+
# <calculatedMemberProperty name="FORMAT_STRING" value="$#,##0.00"/>
|
26
|
+
# </calculatedMember>
|
27
|
+
# </cube>
|
28
|
+
# </schema>
|
29
|
+
|
30
|
+
describe 'A Mondrian XML Schema' do
|
31
|
+
let(:described_class) { ::Rubiks::Schema }
|
32
|
+
let(:schema_hash) {
|
33
|
+
{
|
34
|
+
'cubes' => [{
|
35
|
+
'name' => 'sales',
|
36
|
+
'measures' => [
|
37
|
+
{
|
38
|
+
'name' => 'unit_sales',
|
39
|
+
'aggregator' => 'sum',
|
40
|
+
'format_string' => '#,###'
|
41
|
+
}
|
42
|
+
],
|
43
|
+
'dimensions' => [
|
44
|
+
{
|
45
|
+
'name' => 'date',
|
46
|
+
'hierarchies' => [{
|
47
|
+
'name' => 'year_quarter_month',
|
48
|
+
'levels' => [
|
49
|
+
{
|
50
|
+
'name' => 'year',
|
51
|
+
'type' => 'numeric'
|
52
|
+
},
|
53
|
+
{
|
54
|
+
'name' => 'quarter',
|
55
|
+
'type' => 'string'
|
56
|
+
},
|
57
|
+
{
|
58
|
+
'name' => 'month',
|
59
|
+
'type' => 'numeric'
|
60
|
+
}
|
61
|
+
]
|
62
|
+
}]
|
63
|
+
}
|
64
|
+
],
|
65
|
+
'calculated_members' => [
|
66
|
+
{
|
67
|
+
'name' => 'Profit',
|
68
|
+
'dimension' => 'Measures',
|
69
|
+
'formula' => '[Measures].[Store Sales] - [Measures].[Store Cost]',
|
70
|
+
'format_string' => '$#,##0.00'
|
71
|
+
}
|
72
|
+
]
|
73
|
+
}]
|
74
|
+
}
|
75
|
+
}
|
76
|
+
|
77
|
+
subject { described_class.new_from_hash(schema_hash) }
|
78
|
+
|
79
|
+
describe '#to_xml' do
|
80
|
+
it 'renders XML' do
|
81
|
+
subject.to_xml.should be_like <<-XML
|
82
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
83
|
+
|
84
|
+
<schema name="default">
|
85
|
+
<cube name="Sales">
|
86
|
+
<table name="view_sales"/>
|
87
|
+
|
88
|
+
<dimension name="Date" foreignKey="date_id">
|
89
|
+
<hierarchy name="Year Quarter Month" primaryKey="id" hasAll="true">
|
90
|
+
<table name="view_dates"/>
|
91
|
+
<level name="Year" column="year"/>
|
92
|
+
<level name="Quarter" column="quarter"/>
|
93
|
+
<level name="Month" column="month"/>
|
94
|
+
</hierarchy>
|
95
|
+
</dimension>
|
96
|
+
|
97
|
+
<measure name="Unit Sales" aggregator="sum" formatString="#,###" column="unit_sales"/>
|
98
|
+
|
99
|
+
<calculatedMember name="Profit" dimension="Measures" formula="[Measures].[Store Sales] - [Measures].[Store Cost]">
|
100
|
+
<calculatedMemberProperty name="FORMAT_STRING" value="$#,##0.00"/>
|
101
|
+
</calculatedMember>
|
102
|
+
</cube>
|
103
|
+
</schema>
|
104
|
+
XML
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
# This schema is a valid XML schema for Mondrian
|
4
|
+
#
|
5
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
6
|
+
# <schema name="default">
|
7
|
+
# <cube name="Transactions">
|
8
|
+
# <table name="view_transactions"/>
|
9
|
+
# <dimension name="Date" foreignKey="date_id">
|
10
|
+
# <hierarchy name="Year Quarter Month" primaryKey="id" hasAll="true">
|
11
|
+
# <table name="view_dates"/>
|
12
|
+
# <level name="Year" column="year"/>
|
13
|
+
# <level name="Quarter" column="quarter"/>
|
14
|
+
# <level name="Month" column="month"/>
|
15
|
+
# </hierarchy>
|
16
|
+
# </dimension>
|
17
|
+
# <measure name="Amount" column="amount" aggregator="sum" formatString="$#,###"/>
|
18
|
+
# </cube>
|
19
|
+
# </schema>
|
20
|
+
|
21
|
+
describe 'A simple Mondrian XML Schema' do
|
22
|
+
let(:described_class) { ::Rubiks::Schema }
|
23
|
+
let(:schema_hash) {
|
24
|
+
{
|
25
|
+
'cubes' => [{
|
26
|
+
'name' => 'transactions',
|
27
|
+
'measures' => [
|
28
|
+
{
|
29
|
+
'name' => 'amount',
|
30
|
+
'aggregator' => 'sum',
|
31
|
+
'format_string' => '$#,###'
|
32
|
+
}
|
33
|
+
],
|
34
|
+
'dimensions' => [
|
35
|
+
{
|
36
|
+
'name' => 'date',
|
37
|
+
'hierarchies' => [{
|
38
|
+
'name' => 'year_quarter_month',
|
39
|
+
'levels' => [
|
40
|
+
{
|
41
|
+
'name' => 'year',
|
42
|
+
'type' => 'numeric'
|
43
|
+
},
|
44
|
+
{
|
45
|
+
'name' => 'quarter',
|
46
|
+
'type' => 'numeric'
|
47
|
+
},
|
48
|
+
{
|
49
|
+
'name' => 'month',
|
50
|
+
'type' => 'numeric'
|
51
|
+
}
|
52
|
+
]
|
53
|
+
}]
|
54
|
+
}
|
55
|
+
]
|
56
|
+
}]
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
subject { described_class.new_from_hash(schema_hash) }
|
61
|
+
|
62
|
+
describe '#to_xml' do
|
63
|
+
it 'renders XML' do
|
64
|
+
subject.to_xml.should be_like <<-XML
|
65
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
66
|
+
<schema name="default">
|
67
|
+
<cube name="Transactions">
|
68
|
+
<table name="view_transactions"/>
|
69
|
+
<dimension name="Date" foreignKey="date_id">
|
70
|
+
<hierarchy name="Year Quarter Month" primaryKey="id" hasAll="true">
|
71
|
+
<table name="view_dates"/>
|
72
|
+
<level name="Year" column="year"/>
|
73
|
+
<level name="Quarter" column="quarter"/>
|
74
|
+
<level name="Month" column="month"/>
|
75
|
+
</hierarchy>
|
76
|
+
</dimension>
|
77
|
+
<measure name="Amount" aggregator="sum" formatString="$#,###" column="amount"/>
|
78
|
+
</cube>
|
79
|
+
</schema>
|
80
|
+
XML
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ::Rubiks::CalculatedMember do
|
4
|
+
include_context 'schema_context'
|
5
|
+
|
6
|
+
subject { described_class.new_from_hash }
|
7
|
+
|
8
|
+
specify { subject.respond_to?(:from_hash) }
|
9
|
+
specify { subject.respond_to?(:to_hash) }
|
10
|
+
specify { subject.respond_to?(:dimension) }
|
11
|
+
specify { subject.respond_to?(:formula) }
|
12
|
+
specify { subject.respond_to?(:format_string) }
|
13
|
+
|
14
|
+
context 'when parsed from a valid hash' do
|
15
|
+
subject { described_class.new_from_hash(calculated_member_hash) }
|
16
|
+
|
17
|
+
it { should be_valid }
|
18
|
+
|
19
|
+
its(:to_hash) { should have_key('name') }
|
20
|
+
its(:to_hash) { should have_key('dimension') }
|
21
|
+
its(:to_hash) { should have_key('formula') }
|
22
|
+
its(:to_hash) { should have_key('format_string') }
|
23
|
+
|
24
|
+
|
25
|
+
describe '#to_xml' do
|
26
|
+
it 'renders a calculatedMember tag with attributes' do
|
27
|
+
subject.to_xml.should be_like <<-XML
|
28
|
+
<calculatedMember name="Fake Calculated Member" dimension="Fake Dimension" formula="fake_formula">
|
29
|
+
<calculatedMemberProperty name="FORMAT_STRING" value="$#,##0.00"/>
|
30
|
+
</calculatedMember>
|
31
|
+
XML
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'when parsed from an invalid (empty) hash' do
|
37
|
+
subject { described_class.new_from_hash({}) }
|
38
|
+
|
39
|
+
it { should_not be_valid }
|
40
|
+
|
41
|
+
describe '#to_xml' do
|
42
|
+
it 'renders a calculatedMember tag' do
|
43
|
+
subject.to_xml.should be_like <<-XML
|
44
|
+
<calculatedMember>
|
45
|
+
</calculatedMember>
|
46
|
+
XML
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|