martyr 0.1.74.pre

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