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.
@@ -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
@@ -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
- hash['name'] = self.name.to_s if self.name.present?
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
@@ -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 :column_present, :aggregator_present
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.to_s if self.name.present?
32
- hash['column'] = self.column.to_s if self.column.present?
33
- hash['aggregator'] = self.aggregator.to_s if self.aggregator.present?
34
- hash['format_string'] = self.format_string.to_s if self.format_string.present?
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 column_present
40
- errors << 'Column required on Measure' if self.column.blank?
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.to_s
47
- end
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
@@ -1,16 +1,17 @@
1
- require 'rubiks/nodes/validated_node'
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::ValidatedNode
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 to_json
53
- MultiJson.dump(to_hash)
54
- end
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
- def to_xml
57
- to_hash.to_xml(:root => 'Schema')
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
@@ -1,3 +1,3 @@
1
1
  module ::Rubiks
2
- VERSION = '0.0.4'
2
+ VERSION = '0.0.5'
3
3
  end
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.description = %q{Define an OLAP schema}
12
- gem.summary = 'Rubiks is a Ruby gem that defines an OLAP schema and can output the schema as XML and JSON.'
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