martyr 0.1.74.pre

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.
Files changed (110) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.tags +868 -0
  7. data/.travis.yml +3 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +265 -0
  11. data/Rakefile +1 -0
  12. data/TODO.txt +54 -0
  13. data/bin/console +62 -0
  14. data/bin/setup +7 -0
  15. data/lib/martyr/base_cube.rb +73 -0
  16. data/lib/martyr/cube.rb +134 -0
  17. data/lib/martyr/dimension_reference.rb +26 -0
  18. data/lib/martyr/errors.rb +20 -0
  19. data/lib/martyr/helpers/delegators.rb +17 -0
  20. data/lib/martyr/helpers/intervals.rb +222 -0
  21. data/lib/martyr/helpers/metric_id_standardizer.rb +47 -0
  22. data/lib/martyr/helpers/registrable.rb +15 -0
  23. data/lib/martyr/helpers/sorter.rb +79 -0
  24. data/lib/martyr/helpers/translations.rb +34 -0
  25. data/lib/martyr/level_concern/has_level_collection.rb +11 -0
  26. data/lib/martyr/level_concern/level.rb +45 -0
  27. data/lib/martyr/level_concern/level_collection.rb +60 -0
  28. data/lib/martyr/level_concern/level_comparator.rb +45 -0
  29. data/lib/martyr/level_concern/level_definitions_by_dimension.rb +24 -0
  30. data/lib/martyr/runtime/data_set/coordinates.rb +108 -0
  31. data/lib/martyr/runtime/data_set/element.rb +66 -0
  32. data/lib/martyr/runtime/data_set/element_common.rb +51 -0
  33. data/lib/martyr/runtime/data_set/element_locator.rb +143 -0
  34. data/lib/martyr/runtime/data_set/fact.rb +83 -0
  35. data/lib/martyr/runtime/data_set/fact_indexer.rb +72 -0
  36. data/lib/martyr/runtime/data_set/future_fact_value.rb +58 -0
  37. data/lib/martyr/runtime/data_set/future_metric.rb +40 -0
  38. data/lib/martyr/runtime/data_set/virtual_element.rb +131 -0
  39. data/lib/martyr/runtime/data_set/virtual_elements_builder.rb +202 -0
  40. data/lib/martyr/runtime/dimension_scopes/base_level_scope.rb +20 -0
  41. data/lib/martyr/runtime/dimension_scopes/degenerate_level_scope.rb +76 -0
  42. data/lib/martyr/runtime/dimension_scopes/dimension_scope_collection.rb +78 -0
  43. data/lib/martyr/runtime/dimension_scopes/level_scope_collection.rb +20 -0
  44. data/lib/martyr/runtime/dimension_scopes/query_level_scope.rb +223 -0
  45. data/lib/martyr/runtime/fact_scopes/base_fact_scope.rb +62 -0
  46. data/lib/martyr/runtime/fact_scopes/fact_scope_collection.rb +127 -0
  47. data/lib/martyr/runtime/fact_scopes/main_fact_scope.rb +7 -0
  48. data/lib/martyr/runtime/fact_scopes/null_scope.rb +7 -0
  49. data/lib/martyr/runtime/fact_scopes/sub_fact_scope.rb +16 -0
  50. data/lib/martyr/runtime/fact_scopes/wrapped_fact_scope.rb +11 -0
  51. data/lib/martyr/runtime/pivot/pivot_axis.rb +67 -0
  52. data/lib/martyr/runtime/pivot/pivot_cell.rb +54 -0
  53. data/lib/martyr/runtime/pivot/pivot_grain_element.rb +22 -0
  54. data/lib/martyr/runtime/pivot/pivot_row.rb +49 -0
  55. data/lib/martyr/runtime/pivot/pivot_table.rb +109 -0
  56. data/lib/martyr/runtime/pivot/pivot_table_builder.rb +125 -0
  57. data/lib/martyr/runtime/query/metric_dependency_resolver.rb +149 -0
  58. data/lib/martyr/runtime/query/query_context.rb +246 -0
  59. data/lib/martyr/runtime/query/query_context_builder.rb +215 -0
  60. data/lib/martyr/runtime/scope_operators/base_operator.rb +113 -0
  61. data/lib/martyr/runtime/scope_operators/group_operator.rb +18 -0
  62. data/lib/martyr/runtime/scope_operators/select_operator_for_dimension.rb +24 -0
  63. data/lib/martyr/runtime/scope_operators/select_operator_for_metric.rb +35 -0
  64. data/lib/martyr/runtime/scope_operators/where_operator_for_dimension.rb +43 -0
  65. data/lib/martyr/runtime/scope_operators/where_operator_for_metric.rb +25 -0
  66. data/lib/martyr/runtime/slices/data_slices/data_slice.rb +70 -0
  67. data/lib/martyr/runtime/slices/data_slices/metric_data_slice.rb +54 -0
  68. data/lib/martyr/runtime/slices/data_slices/plain_dimension_data_slice.rb +109 -0
  69. data/lib/martyr/runtime/slices/data_slices/time_dimension_data_slice.rb +9 -0
  70. data/lib/martyr/runtime/slices/has_scoped_levels.rb +29 -0
  71. data/lib/martyr/runtime/slices/memory_slices/TO_DELETE.md +188 -0
  72. data/lib/martyr/runtime/slices/memory_slices/memory_slice.rb +84 -0
  73. data/lib/martyr/runtime/slices/memory_slices/metric_memory_slice.rb +59 -0
  74. data/lib/martyr/runtime/slices/memory_slices/plain_dimension_memory_slice.rb +48 -0
  75. data/lib/martyr/runtime/slices/scopeable_slice_data.rb +73 -0
  76. data/lib/martyr/runtime/slices/slice_definitions/base_slice_definition.rb +30 -0
  77. data/lib/martyr/runtime/slices/slice_definitions/metric_slice_definition.rb +120 -0
  78. data/lib/martyr/runtime/slices/slice_definitions/plain_dimension_level_slice_definition.rb +26 -0
  79. data/lib/martyr/runtime/sub_cubes/fact_filler_strategies.rb +61 -0
  80. data/lib/martyr/runtime/sub_cubes/query_metrics.rb +56 -0
  81. data/lib/martyr/runtime/sub_cubes/sub_cube.rb +134 -0
  82. data/lib/martyr/runtime/sub_cubes/sub_cube_grain.rb +117 -0
  83. data/lib/martyr/schema/dimension_associations/dimension_association_collection.rb +33 -0
  84. data/lib/martyr/schema/dimension_associations/level_association.rb +37 -0
  85. data/lib/martyr/schema/dimension_associations/level_association_collection.rb +18 -0
  86. data/lib/martyr/schema/dimensions/dimension_definition_collection.rb +39 -0
  87. data/lib/martyr/schema/dimensions/plain_dimension_definition.rb +39 -0
  88. data/lib/martyr/schema/dimensions/time_dimension_definition.rb +24 -0
  89. data/lib/martyr/schema/facts/base_fact_definition.rb +22 -0
  90. data/lib/martyr/schema/facts/fact_definition_collection.rb +44 -0
  91. data/lib/martyr/schema/facts/main_fact_definition.rb +45 -0
  92. data/lib/martyr/schema/facts/sub_fact_definition.rb +44 -0
  93. data/lib/martyr/schema/metrics/base_metric.rb +77 -0
  94. data/lib/martyr/schema/metrics/built_in_metric.rb +38 -0
  95. data/lib/martyr/schema/metrics/count_distinct_metric.rb +172 -0
  96. data/lib/martyr/schema/metrics/custom_metric.rb +26 -0
  97. data/lib/martyr/schema/metrics/custom_rollup.rb +31 -0
  98. data/lib/martyr/schema/metrics/dependency_inferrer.rb +150 -0
  99. data/lib/martyr/schema/metrics/metric_definition_collection.rb +94 -0
  100. data/lib/martyr/schema/named_scopes/named_scope.rb +19 -0
  101. data/lib/martyr/schema/named_scopes/named_scope_collection.rb +42 -0
  102. data/lib/martyr/schema/plain_dimension_levels/base_level_definition.rb +39 -0
  103. data/lib/martyr/schema/plain_dimension_levels/degenerate_level_definition.rb +75 -0
  104. data/lib/martyr/schema/plain_dimension_levels/level_definition_collection.rb +15 -0
  105. data/lib/martyr/schema/plain_dimension_levels/query_level_definition.rb +99 -0
  106. data/lib/martyr/version.rb +3 -0
  107. data/lib/martyr/virtual_cube.rb +74 -0
  108. data/lib/martyr.rb +55 -0
  109. data/martyr.gemspec +41 -0
  110. metadata +296 -0
@@ -0,0 +1,131 @@
1
+ module Martyr
2
+ module Runtime
3
+
4
+ # A virtual element is an element that does not really exist, because its grain is comprised of multiple cubes
5
+ # that do not make sense together.
6
+ #
7
+ # It is only useful in the context of relaxing some dimensions in the process of building totals.
8
+ #
9
+ # Example:
10
+ #
11
+ # Cube 1
12
+ # genres.name media_types.name playlists.name tracks_count
13
+ # Rock CD My playlist 10
14
+ #
15
+ # Cube 2
16
+ # genres.name media_types.name customers.country units_sold
17
+ # Rock CD France 95
18
+ #
19
+ # Virtual elements
20
+ # genres.name media_types.name playlists.name customers.country tracks_count units_sold
21
+ # Rock CD My playlist
22
+ #
23
+
24
+ class VirtualElement
25
+ include Martyr::Runtime::ElementCommon
26
+
27
+ attr_reader :grain_hash, :locators, :real_elements, :memory_slice, :future_virtual_metrics_hash
28
+ delegate :inspect, :has_key?, to: :to_hash
29
+ delegate :store, to: :future_virtual_metrics_hash
30
+
31
+ def to_hash
32
+ grains_and_virtual_metrics = grain_hash.merge(future_virtual_metrics_hash)
33
+ real_elements.inject(grains_and_virtual_metrics) {|h, element| h.merge element.to_hash}
34
+ end
35
+
36
+ # @param memory_slice [MemorySlice] cross cubes memory slice
37
+ # @option real_elements [Array<Element>] note that an empty array is a valid input, representing an empty virtual
38
+ # element.
39
+ def initialize(grain_hash, memory_slice, locators, real_elements = nil)
40
+ @grain_hash = grain_hash
41
+ @memory_slice = memory_slice
42
+ @locators = locators
43
+ @real_elements = real_elements || find_real_elements(:get)
44
+ @future_virtual_metrics_hash = {}
45
+ end
46
+
47
+ def empty?
48
+ real_elements.empty?
49
+ end
50
+
51
+ def coordinates
52
+ coordinates_object.to_hash
53
+ end
54
+
55
+ def coordinates_object
56
+ representative.coordinates_object
57
+ end
58
+
59
+ def facts(cube_name = nil)
60
+ facts = Hash[real_elements.map {|x| [x.cube_name, x.facts]}]
61
+ cube_name ? facts[cube_name] : facts
62
+ end
63
+
64
+ def fetch(key)
65
+ send(:[], key)
66
+ end
67
+
68
+ def [](key)
69
+ value = to_hash[key]
70
+ value.is_a?(FutureMetric) ? value.value : value
71
+ end
72
+
73
+ def locate(*args)
74
+ new_real_elements = find_real_elements(:locate, *args)
75
+ VirtualElement.new(representative(*args).grain_hash, memory_slice, locators, new_real_elements)
76
+ end
77
+
78
+ def key_for(level_id)
79
+ representative.key_for(level_id)
80
+ end
81
+
82
+ def record_for(level_id)
83
+ representative.record_for(level_id)
84
+ end
85
+
86
+ def grain_level_ids
87
+ grain_hash.keys
88
+ end
89
+
90
+ def has_metric_id?(metric_id)
91
+ metric_ids.include?(metric_id)
92
+ end
93
+
94
+ def warnings
95
+ arr = real_elements.flat_map do |elm|
96
+ elm.metrics.map{|metric| [metric.id, warning(metric.id)]}
97
+ end
98
+ Hash[arr]
99
+ end
100
+
101
+ def warning(metric_id)
102
+ unsupported_keys_in_memory_slice = memory_slice.keys - coordinates(metric_id).keys
103
+ return [] if unsupported_keys_in_memory_slice.blank?
104
+ "Metric `#{metric_id}` does not support slice on: `#{unsupported_keys_in_memory_slice.join('`, `')}`"
105
+ end
106
+
107
+ def load
108
+ real_elements.try(:each, &:load)
109
+ future_virtual_metrics_hash.values.each(&:value)
110
+ self
111
+ end
112
+
113
+ private
114
+
115
+ # @param method_name [:get, :locate]
116
+ # @param args [Array] args for locate
117
+ def find_real_elements(method_name, *args)
118
+ locators.map do |locator|
119
+ locator.send(method_name, grain_hash, *args)
120
+ end.reject(&:empty?).compact
121
+ end
122
+
123
+ # Allows falling back to an empty real element if no real elements are found to calculate the new grain hash
124
+ # @return [Element] could be empty element
125
+ def representative(*locate_args)
126
+ real_elements.first || locators.first.locate(grain_hash, *locate_args)
127
+ end
128
+
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,202 @@
1
+ module Martyr
2
+ module Runtime
3
+ class VirtualElementsBuilder
4
+ include Martyr::Translations
5
+
6
+ def initialize(memory_slice, unsliced_level_ids_in_grain:, virtual_metrics:)
7
+ @entries = []
8
+ @unsliced_level_ids_in_grain = unsliced_level_ids_in_grain
9
+ @memory_slice = memory_slice
10
+ @lookups_by_level_id = {}
11
+ @virtual_metrics = virtual_metrics
12
+ end
13
+
14
+ # @param elements [Array<Element>]
15
+ # @param sliced [Boolean] true if cube has at least one sliced level or metric
16
+ def add(elements, sliced:)
17
+ return unless elements.present?
18
+ @entries << ElementsFromSubCube.new(elements, sliced, @lookups_by_level_id, @unsliced_level_ids_in_grain)
19
+ end
20
+
21
+ # Let's take 3 cubes:
22
+ #
23
+ # Schools:
24
+ # Country
25
+ # City
26
+ # Name
27
+ #
28
+ # Courses:
29
+ # Topic
30
+ # Course name
31
+ #
32
+ # Students:
33
+ # Country
34
+ # City
35
+ # Name
36
+ #
37
+ # Cube 1 - city budgets for schools
38
+ # (D) schools.country
39
+ # (D) schools.city
40
+ # (M) budget
41
+ #
42
+ # Cube 2 - Student performance
43
+ # (D) schools.country
44
+ # (D) schools.city
45
+ # (D) schools.name
46
+ # (D) students.country
47
+ # (D) students.city
48
+ # (D) students.name
49
+ # (D) courses.topic
50
+ # (D) courses.name
51
+ # (M) grade
52
+ #
53
+ # Cube 3 - International statistics
54
+ # (D) courses.topic
55
+ # (M) importance_factor
56
+ #
57
+ # Cube 4 = National statistics
58
+ # (D) schools.country
59
+ # (D) courses.topic
60
+ # (M) national_importance_factor
61
+ #
62
+ # = Reports
63
+ #
64
+ # City budget vs. average grade
65
+ # schools.country schools.city courses.topic budget average grade
66
+ # USA Boston (total) 1500 8.3
67
+ # USA Boston Math 7.5
68
+ # USA Boston Literature 9.2
69
+ #
70
+ # Cube 3:
71
+ # Budgets for cities that have schools with courses of high international importance factor
72
+ # => NOT SUPPORTED. Cube 3 cannot be added because it shares no grain level
73
+ # schools.country schools.city budget
74
+ # USA Boston 1500
75
+ #
76
+ # Alternative via Cube 4:
77
+ # Budgets for cities whose countries have schools with courses of high national importance factor
78
+ #
79
+ # Slices on cubes 1 and 3 get applied on cube 2:
80
+ # Cube3.importance_factor = high (courses with high importance factor)
81
+ # Cube1.budget > 1000 (countries with at least one city budget bigger than 1000)
82
+ # schools.country = [USA, France, ...]
83
+ #
84
+ # schools.country courses.topic grade
85
+ #
86
+ #
87
+ # The algorithm is as follows:
88
+ # Go over every "sliced cube" - a cube that has at least one sliced level or metric.
89
+ # Take every dependent level and restrict its values
90
+ # Apply the restricted on every cube, including the dependent ones
91
+ #
92
+ # A cube that has no grain and no metric doesn't matter - it can't help with the dependent elements.
93
+ #
94
+ # USA
95
+ # schools.city
96
+ #
97
+ #
98
+ # Authors Posts Comments Reactions
99
+ # Country City Genre Name Type Rating Rating
100
+ #
101
+ # Cube 1: L1.1 L1.2 L2.1 L2.2 L3.1 L3.2
102
+ # Cube 2: L2.1 L2.2 L3.1
103
+ # Cube 3: L3.1 L3.2 L4.1
104
+ #
105
+ # Scenarios:
106
+ # L1.1 L1.2 L2.1 L2.2 L3.1 L3.2 L4.1
107
+ # 1-Slice x x
108
+ # 1-Grain x x
109
+ # #=> Show comment count by post name and comment type for all posts made by authors from Boston with a positive reaction.
110
+ #
111
+ # 2-Slice x
112
+ # 2-Grain x x
113
+ # #=> Show comment count by post name and comment rating for the post named 'My Thoughts'
114
+ #
115
+ #
116
+ # When we are in #build, the memory slice already occurred, meaning it restricted the elements from cube 1
117
+ # to include only the relevant (L2.2, L3.1) combinations and elements from cube 3 to include only relevant
118
+ # (L3.1) elements.
119
+ # #build then looks at the +intersection+ of L2.2 values and L3.1 values across all cubes.
120
+ #
121
+ def build
122
+ return @entries.first.elements if @entries.length == 1
123
+ @entries.each(&:restrict)
124
+
125
+ @entries.flat_map do |entry|
126
+ entry.restricted_elements.map do |element|
127
+ next if (level_ids_in_grain - element.grain_level_ids).present?
128
+ element.grain_hash
129
+ end.compact
130
+ end.uniq.map do |grain_hash|
131
+ elm = VirtualElement.new grain_hash, @memory_slice, @entries.map(&:element_locator)
132
+ elm.rollup(*@virtual_metrics)
133
+ end
134
+ end
135
+
136
+ def level_ids_in_grain
137
+ @level_ids_in_grain ||= @entries.map(&:level_ids).flatten.uniq.sort_by{|x| first_element_from_id(x)}
138
+ end
139
+
140
+ def grain_hash_with_nils
141
+ Hash[level_ids_in_grain.map{|level_id| [level_id, nil]}]
142
+ end
143
+
144
+ class ElementsFromSubCube
145
+ include ActiveModel::Model
146
+
147
+ attr_accessor :element_locator, :elements, :sliced
148
+ attr_reader :lookups_by_level_id, :unsliced_level_ids_in_grain, :level_ids
149
+
150
+ def initialize(elements, sliced, lookups_by_level_id, unsliced_level_ids_in_grain)
151
+ @elements = elements
152
+ @sliced = sliced
153
+ @lookups_by_level_id = lookups_by_level_id
154
+
155
+ representative = @elements.first
156
+ @element_locator = representative.element_locator
157
+ @level_ids = representative.grain_level_ids
158
+ @unsliced_level_ids_in_grain = @level_ids - (@level_ids - unsliced_level_ids_in_grain)
159
+ end
160
+
161
+ def restrict
162
+ return unless sliced
163
+ unsliced_level_ids_in_grain.each do |level_id|
164
+ restrict_one_level(level_id)
165
+ end
166
+ end
167
+
168
+ # @return [Array<Element>] elements whose non-sliced levels are restricted to the
169
+ # intersection of all participating cubes
170
+ def restricted_elements
171
+ unsliced_level_ids_in_grain.inject(elements) do |selected_elements, level_id|
172
+ selected_elements.select do |element|
173
+ restricted_values = lookups_by_level_id[level_id]
174
+ next(true) unless restricted_values.present?
175
+
176
+ value_in_element_for_level = element[level_id]
177
+ restricted_values[value_in_element_for_level]
178
+ end
179
+ end
180
+ end
181
+
182
+ private
183
+
184
+ def restrict_one_level(level_id)
185
+ if lookups_by_level_id.has_key?(level_id)
186
+ # Restrict if the level already exists
187
+ lookups_by_level_id[level_id].slice! *(lookups_by_level_id[level_id].keys & values_for(level_id))
188
+ else
189
+ # Add to lookup if this is first time the level_id is encountered
190
+ lookup_arr = values_for(level_id).map{|x| [x, true]}
191
+ lookups_by_level_id[level_id] = Hash[lookup_arr]
192
+ end
193
+ end
194
+
195
+ def values_for(level_id)
196
+ elements.map{|elm| elm[level_id]}
197
+ end
198
+ end
199
+
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,20 @@
1
+ module Martyr
2
+ module Runtime
3
+ class BaseLevelScope
4
+ include Martyr::Level
5
+ include Martyr::LevelComparator
6
+
7
+ attr_accessor :level
8
+ attr_reader :collection
9
+ delegate :level_definition, :name, :dimension_name, :query?, :degenerate?, :to_i, to: :level
10
+ delegate :fact_key, :fact_alias, to: :level_definition
11
+
12
+ def initialize(collection, level)
13
+ @collection = collection
14
+ @level = level
15
+ @cache = nil
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,76 @@
1
+ module Martyr
2
+ module Runtime
3
+ class DegenerateLevelScope < BaseLevelScope
4
+
5
+ delegate :query_level_with_finder, :query_level_key, :register_element_helper_methods, to: :level
6
+ delegate :nullify, to: :query_level_below
7
+
8
+ def sliceable?
9
+ !!query_level_below
10
+ end
11
+
12
+ def slice_with(values)
13
+ query_level_below.decorate_scope do |scope|
14
+ query_level_with_finder.call(scope, values)
15
+ end
16
+ query_level_below.set_bottom_sliced_level
17
+ end
18
+
19
+ def loaded?
20
+ query_level_below.loaded?
21
+ end
22
+
23
+ # TODO: load from fact when sliceable? is false
24
+ def load
25
+ return true if loaded?
26
+ query_level_below.load
27
+ true
28
+ end
29
+
30
+ def all
31
+ loaded_cache.keys
32
+ end
33
+ alias_method :keys, :all
34
+ alias_method :all_values, :all
35
+
36
+ # Useful when the fact is attempting to resolve Degenerate 1 value:
37
+ # Degenerate 1
38
+ # Degenerate 2 ---- Cube
39
+ # Query 3
40
+ #
41
+ # @return [String] the value of the "Degenerate 1" parent
42
+ def recursive_lookup_up(degenerate_value, level:)
43
+ return degenerate_value if name == level.name
44
+ query_level_primary_key = query_level_below.record_primary_key(loaded_cache[degenerate_value])
45
+ query_level_below.recursive_lookup_up(query_level_primary_key, level: level)
46
+ end
47
+
48
+ def recursive_lookup_down(degenerate_values, level:)
49
+ degenerate_values = Array.wrap(degenerate_values)
50
+ return degenerate_values if name == level.name
51
+ query_level_records = degenerate_values.flat_map{|value| query_level_below.cached_records_by(query_level_key)[value]}
52
+ query_level_below.recursive_lookup_down(query_level_records, level: level)
53
+ end
54
+
55
+ protected
56
+
57
+ # The cache stored is basically one representative of the query level for each degenerate value.
58
+ # Assuming the hierarchy is strict, i.e. the state parent of "San Francisco" is always "California", we are able
59
+ # to infer correctly the hierarchy
60
+ def cache
61
+ @cache ||= query_level_below.all.index_by{|x| degenerate_value_from_query_record(x) }
62
+ end
63
+
64
+ def loaded_cache
65
+ load and return cache
66
+ end
67
+
68
+ public
69
+
70
+ def degenerate_value_from_query_record(record)
71
+ record.send(query_level_key)
72
+ end
73
+
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,78 @@
1
+ module Martyr
2
+ module Runtime
3
+ class DimensionScopeCollection < HashWithIndifferentAccess
4
+ include Martyr::Registrable
5
+ include Martyr::Translations
6
+
7
+ attr_reader :dimension_definitions
8
+ alias_method :find_dimension, :find_or_error
9
+ alias_method :has_dimension?, :has_key?
10
+
11
+ def initialize(dimension_definitions)
12
+ @dimension_definitions = dimension_definitions
13
+ end
14
+
15
+ def to_s
16
+ values.flat_map{|x| x.level_objects.map(&:id)}.inspect
17
+ end
18
+
19
+ # @param level [Martyr::Level]
20
+ def register_level(level)
21
+ dimension_scope = find_or_nil(level.dimension_name) ||
22
+ register(Martyr::DimensionReference.new(level.dimension_definition, Runtime::LevelScopeCollection))
23
+ dimension_scope.register_level(level)
24
+ end
25
+
26
+ def find_level(level_id)
27
+ with_standard_id(level_id) { |dimension, level| find_dimension(dimension).find_level(level) }
28
+ end
29
+ alias_method :level_scope, :find_level
30
+
31
+ def level_scopes(level_ids)
32
+ Array.wrap(level_ids).map{|x| level_scope(x)}
33
+ end
34
+
35
+ # @param level_id [String]
36
+ # @yieldparam [BaseLevelScope]
37
+ def with_level_scope(level_id)
38
+ yield level_scope(level_id)
39
+ end
40
+
41
+ def level_loaded?(level_id)
42
+ level_scope(level_id).loaded?
43
+ end
44
+
45
+ # @param level_ids [Array<String>, String]
46
+ # @return [Array<BaseLevelScope>] lowest levels from the levels array in each dimension
47
+ def lowest_level_of(level_ids)
48
+ LevelDefinitionsByDimension.new(level_scopes(level_ids)).lowest_levels
49
+ end
50
+
51
+ # @param level_ids [Array<String>, String]
52
+ # @return [String] lowest levels from the levels array in each dimension
53
+ def lowest_level_ids_of(level_ids)
54
+ lowest_level_of(level_ids).map(&:id)
55
+ end
56
+
57
+ # @param level_ids [Array<String>, String]
58
+ # @return [Array<BaseLevelScope>] lowest levels from the levels array in each dimension, and all the level
59
+ # scopes above them
60
+ def levels_and_above_for(level_ids)
61
+ lowest_level_of(level_ids).flat_map(&:level_and_above)
62
+ end
63
+
64
+ # @param level_ids [Array<Martyr::Level>]
65
+ # @return [Array<String>]
66
+ def level_ids_and_above_for(level_ids)
67
+ levels_and_above_for(level_ids).map(&:id)
68
+ end
69
+
70
+ def register_element_helper_methods(mod)
71
+ values.each do |dimension_reference|
72
+ dimension_reference.register_element_helper_methods(mod)
73
+ end
74
+ end
75
+
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,20 @@
1
+ module Martyr
2
+ module Runtime
3
+ class LevelScopeCollection < HashWithIndifferentAccess
4
+ include Martyr::LevelCollection
5
+
6
+ attr_accessor :bottom_level_sliced_i
7
+
8
+ def initialize(*args)
9
+ super
10
+ @bottom_level_sliced_i = nil
11
+ end
12
+
13
+ # @param level [Martyr::Level]
14
+ def register_level(level)
15
+ register level.build(self)
16
+ end
17
+
18
+ end
19
+ end
20
+ end