rubiks 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|