rubiks 0.0.4 → 0.0.5

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