gooddata 0.6.16 → 0.6.17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -1
  3. data/lib/gooddata/cli/commands/project_cmd.rb +1 -1
  4. data/lib/gooddata/core/logging.rb +15 -5
  5. data/lib/gooddata/core/rest.rb +4 -28
  6. data/lib/gooddata/helpers/global_helpers.rb +14 -138
  7. data/lib/gooddata/helpers/global_helpers_params.rb +145 -0
  8. data/lib/gooddata/mixins/md_object_indexer.rb +2 -2
  9. data/lib/gooddata/models/domain.rb +1 -1
  10. data/lib/gooddata/models/execution.rb +29 -1
  11. data/lib/gooddata/models/from_wire.rb +6 -0
  12. data/lib/gooddata/models/from_wire_parse.rb +125 -0
  13. data/lib/gooddata/models/metadata/attribute.rb +1 -1
  14. data/lib/gooddata/models/metadata/label.rb +11 -10
  15. data/lib/gooddata/models/model.rb +4 -0
  16. data/lib/gooddata/models/profile.rb +12 -2
  17. data/lib/gooddata/models/project.rb +6 -3
  18. data/lib/gooddata/models/project_blueprint.rb +4 -4
  19. data/lib/gooddata/models/project_creator.rb +8 -10
  20. data/lib/gooddata/models/report_data_result.rb +4 -2
  21. data/lib/gooddata/models/schedule.rb +121 -66
  22. data/lib/gooddata/models/to_wire.rb +12 -3
  23. data/lib/gooddata/models/user_filters/user_filter_builder.rb +3 -234
  24. data/lib/gooddata/models/user_filters/user_filter_builder_create.rb +115 -0
  25. data/lib/gooddata/models/user_filters/user_filter_builder_execute.rb +133 -0
  26. data/lib/gooddata/rest/client.rb +27 -13
  27. data/lib/gooddata/rest/connection.rb +102 -23
  28. data/lib/gooddata/version.rb +1 -1
  29. data/spec/data/gd_gse_data_blueprint.json +1 -0
  30. data/spec/data/test_project_model_spec.json +5 -2
  31. data/spec/data/wire_models/model_view.json +3 -0
  32. data/spec/data/wire_test_project.json +8 -1
  33. data/spec/integration/full_project_spec.rb +1 -1
  34. data/spec/unit/core/connection_spec.rb +16 -0
  35. data/spec/unit/core/logging_spec.rb +54 -6
  36. data/spec/unit/models/domain_spec.rb +10 -4
  37. data/spec/unit/models/execution_spec.rb +102 -0
  38. data/spec/unit/models/from_wire_spec.rb +11 -2
  39. data/spec/unit/models/model_spec.rb +2 -2
  40. data/spec/unit/models/project_blueprint_spec.rb +1 -1
  41. data/spec/unit/models/schedule_spec.rb +34 -24
  42. data/spec/unit/models/to_wire_spec.rb +9 -1
  43. metadata +8 -3
@@ -18,7 +18,8 @@ module GoodData
18
18
  {
19
19
  attribute: {
20
20
  identifier: GoodData::Model.identifier_for(dataset, type: :anchor_no_label),
21
- title: "Records of #{ GoodData::Model.title(dataset) }"
21
+ title: "Records of #{ GoodData::Model.title(dataset) }",
22
+ folder: dataset[:folder] || GoodData::Model.title(dataset)
22
23
  }
23
24
  }
24
25
  end
@@ -43,10 +44,11 @@ module GoodData
43
44
  def self.attribute_to_wire(dataset, attribute)
44
45
  default_label = DatasetBlueprint.default_label_for_attribute(dataset, attribute)
45
46
  label = default_label[:type].to_sym == :label ? default_label : default_label.merge(type: :primary_label)
46
- {
47
+ payload = {
47
48
  attribute: {
48
49
  identifier: GoodData::Model.identifier_for(dataset, attribute),
49
50
  title: GoodData::Model.title(attribute),
51
+ folder: attribute[:folder] || dataset[:folder] || GoodData::Model.title(dataset),
50
52
  labels: ([attribute.merge(type: :primary_label)] + DatasetBlueprint.labels_for_attribute(dataset, attribute)).map do |l|
51
53
  {
52
54
  label: {
@@ -60,6 +62,9 @@ module GoodData
60
62
  defaultLabel: GoodData::Model.identifier_for(dataset, label, attribute)
61
63
  }
62
64
  }
65
+ payload.tap do |p|
66
+ p[:attribute][:description] = GoodData::Model.description(attribute) if GoodData::Model.description(attribute)
67
+ end
63
68
  end
64
69
 
65
70
  # Converts dataset to wire format.
@@ -100,13 +105,17 @@ module GoodData
100
105
  # @param fact [Hash] Fact blueprint
101
106
  # @return [Hash] Manifest for a particular reference
102
107
  def self.fact_to_wire(dataset, fact)
103
- {
108
+ payload = {
104
109
  fact: {
105
110
  identifier: GoodData::Model.identifier_for(dataset, fact),
106
111
  title: GoodData::Model.title(fact),
112
+ folder: fact[:folder] || dataset[:folder] || GoodData::Model.title(dataset),
107
113
  dataType: fact[:gd_data_type] || DEFAULT_FACT_DATATYPE
108
114
  }
109
115
  }
116
+ payload.tap do |p|
117
+ p[:fact][:description] = GoodData::Model.description(fact) if GoodData::Model.description(fact)
118
+ end
110
119
  end
111
120
 
112
121
  # Converts references to wire format.
@@ -1,5 +1,8 @@
1
1
  # encoding: UTF-8
2
2
 
3
+ require_relative 'user_filter_builder_create'
4
+ require_relative 'user_filter_builder_execute'
5
+
3
6
  module GoodData
4
7
  module UserFilterBuilder
5
8
  # Main Entry function. Gets values and processes them to get filters
@@ -119,116 +122,6 @@ module GoodData
119
122
  end
120
123
  end
121
124
 
122
- def self.create_label_cache(result, options = {})
123
- project = options[:project]
124
-
125
- result.reduce({}) do |a, e|
126
- e[:filters].map do |filter|
127
- a[filter[:label]] = project.labels(filter[:label]) unless a.key?(filter[:label])
128
- end
129
- a
130
- end
131
- end
132
-
133
- def self.create_lookups_cache(small_labels)
134
- small_labels.reduce({}) do |a, e|
135
- lookup = e.values(:limit => 1_000_000).reduce({}) do |a1, e1|
136
- a1[e1[:value]] = e1[:uri]
137
- a1
138
- end
139
- a[e.uri] = lookup
140
- a
141
- end
142
- end
143
-
144
- # Walks over provided labels and picks those that have fewer than certain amount of values
145
- # This tries to balance for speed when working with small datasets (like users)
146
- # so it precaches the values and still be able to function for larger ones even
147
- # though that would mean tons of requests
148
- def self.get_small_labels(labels_cache)
149
- labels_cache.values.select { |label| label.values_count < 100_000 }
150
- end
151
-
152
- # Creates a MAQL expression(s) based on the filter defintion.
153
- # Takes the filter definition looks up any necessary values and provides API executable MAQL
154
- def self.create_expression(filter, labels_cache, lookups_cache, options = {})
155
- errors = []
156
- values = filter[:values]
157
- label = labels_cache[filter[:label]]
158
- element_uris = values.map do |v|
159
- begin
160
- if lookups_cache.key?(label.uri)
161
- if lookups_cache[label.uri].key?(v)
162
- lookups_cache[label.uri][v]
163
- else
164
- fail
165
- end
166
- else
167
- label.find_value_uri(v)
168
- end
169
- rescue
170
- errors << [label.title, v]
171
- nil
172
- end
173
- end
174
- expression = if element_uris.compact.empty? && options[:restrict_if_missing_all_values] && options[:type] == :muf
175
- '1 <> 1'
176
- elsif element_uris.compact.empty? && options[:restrict_if_missing_all_values] && options[:type] == :variable
177
- nil
178
- elsif element_uris.compact.empty?
179
- 'TRUE'
180
- elsif filter[:over] && filter[:to]
181
- "([#{label.attribute_uri}] IN (#{ element_uris.compact.sort.map { |e| '[' + e + ']' }.join(', ') })) OVER [#{filter[:over]}] TO [#{filter[:to]}]"
182
- else
183
- "[#{label.attribute_uri}] IN (#{ element_uris.compact.sort.map { |e| '[' + e + ']' }.join(', ') })"
184
- end
185
- [expression, errors]
186
- end
187
-
188
- # Encapuslates the creation of filter
189
- def self.create_user_filter(expression, related)
190
- {
191
- 'related' => related,
192
- 'level' => :user,
193
- 'expression' => expression,
194
- 'type' => :filter
195
- }
196
- end
197
-
198
- # Resolves and creates maql statements from filter definitions.
199
- # This method does not perform any modifications on API but
200
- # collects all the information that is needed to do so.
201
- # Method collects all info from the user and current state in project and compares.
202
- # Returns suggestion of what should be deleted and what should be created
203
- # If there is some discrepancies in the data (missing values, nonexistent users) it
204
- # finishes and collects all the errors at once
205
- #
206
- # @param filters [Array<Hash>] Filters definition
207
- # @return [Array] first is list of MAQL statements
208
- def self.maqlify_filters(filters, options = {})
209
- project = options[:project]
210
- users_cache = options[:users_cache] || create_cache(project.users, :login)
211
- labels_cache = create_label_cache(filters, options)
212
- small_labels = get_small_labels(labels_cache)
213
- lookups_cache = create_lookups_cache(small_labels)
214
-
215
- errors = []
216
- results = filters.mapcat do |filter|
217
- login = filter[:login]
218
- filter[:filters].mapcat do |f|
219
- expression, error = create_expression(f, labels_cache, lookups_cache, options)
220
- errors << error unless error.empty?
221
- profiles_uri = (users_cache[login] && users_cache[login].uri)
222
- if profiles_uri && expression
223
- [create_user_filter(expression, profiles_uri)]
224
- else
225
- []
226
- end
227
- end
228
- end
229
- [results, errors]
230
- end
231
-
232
125
  def self.resolve_user_filter(user = [], project = [])
233
126
  user ||= []
234
127
  project ||= []
@@ -257,78 +150,6 @@ module GoodData
257
150
  [to_create, to_delete]
258
151
  end
259
152
 
260
- # Executes the update for variables. It resolves what is new and needed to update.
261
- # @param filters [Array<Hash>] Filter Definitions
262
- # @param filters [Variable] Variable instance to be updated
263
- # @param options [Hash]
264
- # @option options [Boolean] :dry_run If dry run is true. No changes to he proejct are made but list of changes is provided
265
- # @return [Array] list of filters that needs to be created and deleted
266
- def self.execute_variables(filters, var, options = {})
267
- client = options[:client]
268
- project = options[:project]
269
- dry_run = options[:dry_run]
270
- to_create, to_delete = execute(filters, var.user_values, VariableUserFilter, options.merge(type: :variable))
271
- return [to_create, to_delete] if dry_run
272
-
273
- # TODO: get values that are about to be deleted and created and update them.
274
- # This will make sure there is no downitme in filter existence
275
- unless options[:do_not_touch_filters_that_are_not_mentioned]
276
- to_delete.each { |_, group| group.each(&:delete) }
277
- end
278
- data = to_create.values.flatten.map(&:to_hash).map { |var_val| var_val.merge(prompt: var.uri) }
279
- data.each_slice(200) do |slice|
280
- client.post("/gdc/md/#{project.obj_id}/variables/user", :variables => slice)
281
- end
282
- [to_create, to_delete]
283
- end
284
-
285
- def self.execute_mufs(filters, options = {})
286
- client = options[:client]
287
- project = options[:project]
288
-
289
- dry_run = options[:dry_run]
290
- to_create, to_delete = execute(filters, project.data_permissions, MandatoryUserFilter, options.merge(type: :muf))
291
- return [to_create, to_delete] if dry_run
292
-
293
- to_create.peach do |related_uri, group|
294
- group.each(&:save)
295
-
296
- res = client.get("/gdc/md/#{project.pid}/userfilters?users=#{related_uri}")
297
- items = res['userFilters']['items'].empty? ? [] : res['userFilters']['items'].first['userFilters']
298
-
299
- payload = {
300
- 'userFilters' => {
301
- 'items' => [{
302
- 'user' => related_uri,
303
- 'userFilters' => items.concat(group.map(&:uri))
304
- }]
305
- }
306
- }
307
- client.post("/gdc/md/#{project.pid}/userfilters", payload)
308
- end
309
- unless options[:do_not_touch_filters_that_are_not_mentioned]
310
- to_delete.peach do |related_uri, group|
311
- if related_uri
312
- res = client.get("/gdc/md/#{project.pid}/userfilters?users=#{related_uri}")
313
- items = res['userFilters']['items'].empty? ? [] : res['userFilters']['items'].first['userFilters']
314
- payload = {
315
- 'userFilters' => {
316
- 'items' => [
317
- {
318
- 'user' => related_uri,
319
- 'userFilters' => items - group.map(&:uri)
320
- }
321
- ]
322
- }
323
- }
324
- client.post("/gdc/md/#{project.pid}/userfilters", payload)
325
- end
326
- group.each(&:delete)
327
- end
328
- end
329
- [to_create, to_delete]
330
- end
331
-
332
153
  private
333
154
 
334
155
  # Reads values from File/Array. Abstracts away the fact if it is column based,
@@ -354,57 +175,5 @@ module GoodData
354
175
  end
355
176
  memo
356
177
  end
357
-
358
- # Executes the procedure necessary for loading user filters. This method has what
359
- # is common for both implementations. Funcion
360
- # * makes sure that filters are in normalized form.
361
- # * verifies that users are in the project (and domain)
362
- # * creates maql expressions of the filters provided
363
- # * resolves the filters against current values in the project
364
- # @param user_filters [Array] Filters that user is trying to set up
365
- # @param project_filters [Array] List of filters currently in the project
366
- # @param klass [Class] Class can be aither UserFilter or VariableFilter
367
- # @param options [Hash] Filter definitions
368
- # @return [Array<Hash>]
369
- def self.execute(user_filters, project_filters, klass, options = {})
370
- client = options[:client]
371
- project = options[:project]
372
-
373
- ignore_missing_values = options[:ignore_missing_values]
374
- users_must_exist = options[:users_must_exist] == false ? false : true
375
- filters = normalize_filters(user_filters)
376
- domain = options[:domain]
377
- users = domain ? project.users + domain.users : project.users
378
- users_cache = create_cache(users, :login)
379
- verify_existing_users(filters, options.merge(users_must_exist: users_must_exist, users_cache: users_cache))
380
- user_filters, errors = maqlify_filters(filters, options.merge(users_cache: users_cache, users_must_exist: users_must_exist))
381
- fail "Validation failed #{errors}" if !ignore_missing_values && !errors.empty?
382
-
383
- filters = user_filters.map { |data| client.create(klass, data, project: project) }
384
- resolve_user_filters(filters, project_filters)
385
- end
386
-
387
- # Gets definition of filters from user. They might either come in the full definition
388
- # as hash or a simplified version. The simplified version do not cover all the possible
389
- # features but it is much simpler to remember and suitable for quick hacking around
390
- # @param filters [Array<Array | Hash>]
391
- # @return [Array<Hash>]
392
- def self.normalize_filters(filters)
393
- filters.map do |filter|
394
- if filter.is_a?(Hash)
395
- filter
396
- else
397
- {
398
- :login => filter.first,
399
- :filters => [
400
- {
401
- :label => filter[1],
402
- :values => filter[2..-1]
403
- }
404
- ]
405
- }
406
- end
407
- end
408
- end
409
178
  end
410
179
  end
@@ -0,0 +1,115 @@
1
+ # encoding: UTF-8
2
+
3
+ module GoodData
4
+ module UserFilterBuilder
5
+ def self.create_label_cache(result, options = {})
6
+ project = options[:project]
7
+
8
+ result.reduce({}) do |a, e|
9
+ e[:filters].map do |filter|
10
+ a[filter[:label]] = project.labels(filter[:label]) unless a.key?(filter[:label])
11
+ end
12
+ a
13
+ end
14
+ end
15
+
16
+ def self.create_lookups_cache(small_labels)
17
+ small_labels.reduce({}) do |a, e|
18
+ lookup = e.values(:limit => 1_000_000).reduce({}) do |a1, e1|
19
+ a1[e1[:value]] = e1[:uri]
20
+ a1
21
+ end
22
+ a[e.uri] = lookup
23
+ a
24
+ end
25
+ end
26
+
27
+ # Walks over provided labels and picks those that have fewer than certain amount of values
28
+ # This tries to balance for speed when working with small datasets (like users)
29
+ # so it precaches the values and still be able to function for larger ones even
30
+ # though that would mean tons of requests
31
+ def self.get_small_labels(labels_cache)
32
+ labels_cache.values.select { |label| label.values_count < 100_000 }
33
+ end
34
+
35
+ # Creates a MAQL expression(s) based on the filter defintion.
36
+ # Takes the filter definition looks up any necessary values and provides API executable MAQL
37
+ def self.create_expression(filter, labels_cache, lookups_cache, options = {})
38
+ errors = []
39
+ values = filter[:values]
40
+ label = labels_cache[filter[:label]]
41
+ element_uris = values.map do |v|
42
+ begin
43
+ if lookups_cache.key?(label.uri)
44
+ if lookups_cache[label.uri].key?(v)
45
+ lookups_cache[label.uri][v]
46
+ else
47
+ fail
48
+ end
49
+ else
50
+ label.find_value_uri(v)
51
+ end
52
+ rescue
53
+ errors << [label.title, v]
54
+ nil
55
+ end
56
+ end
57
+ expression = if element_uris.compact.empty? && options[:restrict_if_missing_all_values] && options[:type] == :muf
58
+ '1 <> 1'
59
+ elsif element_uris.compact.empty? && options[:restrict_if_missing_all_values] && options[:type] == :variable
60
+ nil
61
+ elsif element_uris.compact.empty?
62
+ 'TRUE'
63
+ elsif filter[:over] && filter[:to]
64
+ "([#{label.attribute_uri}] IN (#{ element_uris.compact.sort.map { |e| '[' + e + ']' }.join(', ') })) OVER [#{filter[:over]}] TO [#{filter[:to]}]"
65
+ else
66
+ "[#{label.attribute_uri}] IN (#{ element_uris.compact.sort.map { |e| '[' + e + ']' }.join(', ') })"
67
+ end
68
+ [expression, errors]
69
+ end
70
+
71
+ # Encapuslates the creation of filter
72
+ def self.create_user_filter(expression, related)
73
+ {
74
+ 'related' => related,
75
+ 'level' => :user,
76
+ 'expression' => expression,
77
+ 'type' => :filter
78
+ }
79
+ end
80
+
81
+ # Resolves and creates maql statements from filter definitions.
82
+ # This method does not perform any modifications on API but
83
+ # collects all the information that is needed to do so.
84
+ # Method collects all info from the user and current state in project and compares.
85
+ # Returns suggestion of what should be deleted and what should be created
86
+ # If there is some discrepancies in the data (missing values, nonexistent users) it
87
+ # finishes and collects all the errors at once
88
+ #
89
+ # @param filters [Array<Hash>] Filters definition
90
+ # @return [Array] first is list of MAQL statements
91
+ def self.maqlify_filters(filters, options = {})
92
+ project = options[:project]
93
+ users_cache = options[:users_cache] || create_cache(project.users, :login)
94
+ labels_cache = create_label_cache(filters, options)
95
+ small_labels = get_small_labels(labels_cache)
96
+ lookups_cache = create_lookups_cache(small_labels)
97
+
98
+ errors = []
99
+ results = filters.mapcat do |filter|
100
+ login = filter[:login]
101
+ filter[:filters].mapcat do |f|
102
+ expression, error = create_expression(f, labels_cache, lookups_cache, options)
103
+ errors << error unless error.empty?
104
+ profiles_uri = (users_cache[login] && users_cache[login].uri)
105
+ if profiles_uri && expression
106
+ [create_user_filter(expression, profiles_uri)]
107
+ else
108
+ []
109
+ end
110
+ end
111
+ end
112
+ [results, errors]
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,133 @@
1
+ # encoding: UTF-8
2
+
3
+ module GoodData
4
+ module UserFilterBuilder
5
+ # Executes the update for variables. It resolves what is new and needed to update.
6
+ # @param filters [Array<Hash>] Filter Definitions
7
+ # @param filters [Variable] Variable instance to be updated
8
+ # @param options [Hash]
9
+ # @option options [Boolean] :dry_run If dry run is true. No changes to he proejct are made but list of changes is provided
10
+ # @return [Array] list of filters that needs to be created and deleted
11
+ def self.execute_variables(filters, var, options = {})
12
+ client = options[:client]
13
+ project = options[:project]
14
+ dry_run = options[:dry_run]
15
+ to_create, to_delete = execute(filters, var.user_values, VariableUserFilter, options.merge(type: :variable))
16
+ return [to_create, to_delete] if dry_run
17
+
18
+ # TODO: get values that are about to be deleted and created and update them.
19
+ # This will make sure there is no downitme in filter existence
20
+ unless options[:do_not_touch_filters_that_are_not_mentioned]
21
+ to_delete.each { |_, group| group.each(&:delete) }
22
+ end
23
+ data = to_create.values.flatten.map(&:to_hash).map { |var_val| var_val.merge(prompt: var.uri) }
24
+ data.each_slice(200) do |slice|
25
+ client.post("/gdc/md/#{project.obj_id}/variables/user", :variables => slice)
26
+ end
27
+ [to_create, to_delete]
28
+ end
29
+
30
+ def self.execute_mufs(filters, options = {})
31
+ client = options[:client]
32
+ project = options[:project]
33
+
34
+ dry_run = options[:dry_run]
35
+ to_create, to_delete = execute(filters, project.data_permissions, MandatoryUserFilter, options.merge(type: :muf))
36
+ return [to_create, to_delete] if dry_run
37
+
38
+ to_create.peach do |related_uri, group|
39
+ group.each(&:save)
40
+
41
+ res = client.get("/gdc/md/#{project.pid}/userfilters?users=#{related_uri}")
42
+ items = res['userFilters']['items'].empty? ? [] : res['userFilters']['items'].first['userFilters']
43
+
44
+ payload = {
45
+ 'userFilters' => {
46
+ 'items' => [{
47
+ 'user' => related_uri,
48
+ 'userFilters' => items.concat(group.map(&:uri))
49
+ }]
50
+ }
51
+ }
52
+ client.post("/gdc/md/#{project.pid}/userfilters", payload)
53
+ end
54
+ unless options[:do_not_touch_filters_that_are_not_mentioned]
55
+ to_delete.peach do |related_uri, group|
56
+ if related_uri
57
+ res = client.get("/gdc/md/#{project.pid}/userfilters?users=#{related_uri}")
58
+ items = res['userFilters']['items'].empty? ? [] : res['userFilters']['items'].first['userFilters']
59
+ payload = {
60
+ 'userFilters' => {
61
+ 'items' => [
62
+ {
63
+ 'user' => related_uri,
64
+ 'userFilters' => items - group.map(&:uri)
65
+ }
66
+ ]
67
+ }
68
+ }
69
+ client.post("/gdc/md/#{project.pid}/userfilters", payload)
70
+ end
71
+ group.each(&:delete)
72
+ end
73
+ end
74
+ [to_create, to_delete]
75
+ end
76
+
77
+ private
78
+
79
+ # Executes the procedure necessary for loading user filters. This method has what
80
+ # is common for both implementations. Funcion
81
+ # * makes sure that filters are in normalized form.
82
+ # * verifies that users are in the project (and domain)
83
+ # * creates maql expressions of the filters provided
84
+ # * resolves the filters against current values in the project
85
+ # @param user_filters [Array] Filters that user is trying to set up
86
+ # @param project_filters [Array] List of filters currently in the project
87
+ # @param klass [Class] Class can be aither UserFilter or VariableFilter
88
+ # @param options [Hash] Filter definitions
89
+ # @return [Array<Hash>]
90
+ def self.execute(user_filters, project_filters, klass, options = {})
91
+ client = options[:client]
92
+ project = options[:project]
93
+
94
+ ignore_missing_values = options[:ignore_missing_values]
95
+ users_must_exist = options[:users_must_exist] == false ? false : true
96
+ filters = normalize_filters(user_filters)
97
+ domain = options[:domain]
98
+ users = domain ? project.users + domain.users : project.users
99
+ users_cache = create_cache(users, :login)
100
+ verify_existing_users(filters, options.merge(users_must_exist: users_must_exist, users_cache: users_cache))
101
+ user_filters, errors = maqlify_filters(filters, options.merge(users_cache: users_cache, users_must_exist: users_must_exist))
102
+ fail "Validation failed #{errors}" if !ignore_missing_values && !errors.empty?
103
+
104
+ filters = user_filters.map { |data| client.create(klass, data, project: project) }
105
+ resolve_user_filters(filters, project_filters)
106
+ end
107
+
108
+ private
109
+
110
+ # Gets definition of filters from user. They might either come in the full definition
111
+ # as hash or a simplified version. The simplified version do not cover all the possible
112
+ # features but it is much simpler to remember and suitable for quick hacking around
113
+ # @param filters [Array<Array | Hash>]
114
+ # @return [Array<Hash>]
115
+ def self.normalize_filters(filters)
116
+ filters.map do |filter|
117
+ if filter.is_a?(Hash)
118
+ filter
119
+ else
120
+ {
121
+ :login => filter.first,
122
+ :filters => [
123
+ {
124
+ :label => filter[1],
125
+ :values => filter[2..-1]
126
+ }
127
+ ]
128
+ }
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end