gooddata 0.6.16 → 0.6.17

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 (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