gooddata 0.6.19 → 0.6.20

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/Rakefile +17 -3
  4. data/gooddata.gemspec +8 -7
  5. data/lib/gooddata/bricks/middleware/base_middleware.rb +1 -1
  6. data/lib/gooddata/cli/commands/run_ruby_cmd.rb +2 -2
  7. data/lib/gooddata/cli/shared.rb +2 -1
  8. data/lib/gooddata/commands/auth.rb +58 -5
  9. data/lib/gooddata/commands/runners.rb +2 -6
  10. data/lib/gooddata/extensions/big_decimal.rb +4 -0
  11. data/lib/gooddata/extensions/false.rb +11 -0
  12. data/lib/gooddata/extensions/hash.rb +6 -17
  13. data/lib/gooddata/extensions/nil.rb +11 -0
  14. data/lib/gooddata/extensions/numeric.rb +11 -0
  15. data/lib/gooddata/extensions/object.rb +11 -0
  16. data/lib/gooddata/extensions/symbol.rb +11 -0
  17. data/lib/gooddata/extensions/true.rb +11 -0
  18. data/lib/gooddata/helpers/auth_helpers.rb +32 -2
  19. data/lib/gooddata/helpers/data_helper.rb +1 -1
  20. data/lib/gooddata/helpers/global_helpers.rb +98 -31
  21. data/lib/gooddata/mixins/md_finders.rb +15 -15
  22. data/lib/gooddata/mixins/md_object_query.rb +12 -2
  23. data/lib/gooddata/models/blueprint/blueprint_field.rb +2 -2
  24. data/lib/gooddata/models/blueprint/dataset_blueprint.rb +2 -2
  25. data/lib/gooddata/models/blueprint/project_blueprint.rb +3 -3
  26. data/lib/gooddata/models/blueprint/schema_blueprint.rb +1 -1
  27. data/lib/gooddata/models/datawarehouse.rb +1 -0
  28. data/lib/gooddata/models/domain.rb +13 -16
  29. data/lib/gooddata/models/from_wire.rb +0 -2
  30. data/lib/gooddata/models/membership.rb +1 -1
  31. data/lib/gooddata/models/metadata/attribute.rb +1 -1
  32. data/lib/gooddata/models/metadata/dashboard.rb +1 -1
  33. data/lib/gooddata/models/metadata/dataset.rb +1 -1
  34. data/lib/gooddata/models/metadata/dimension.rb +1 -1
  35. data/lib/gooddata/models/metadata/fact.rb +1 -1
  36. data/lib/gooddata/models/metadata/label.rb +16 -17
  37. data/lib/gooddata/models/metadata/metric.rb +1 -1
  38. data/lib/gooddata/models/metadata/report.rb +1 -1
  39. data/lib/gooddata/models/metadata/report_definition.rb +7 -7
  40. data/lib/gooddata/models/metadata/variable.rb +1 -1
  41. data/lib/gooddata/models/model.rb +2 -2
  42. data/lib/gooddata/models/profile.rb +2 -2
  43. data/lib/gooddata/models/project.rb +21 -23
  44. data/lib/gooddata/models/project_role.rb +3 -3
  45. data/lib/gooddata/models/schedule.rb +18 -4
  46. data/lib/gooddata/models/user_filters/mandatory_user_filter.rb +12 -15
  47. data/lib/gooddata/models/user_filters/user_filter.rb +8 -8
  48. data/lib/gooddata/models/user_filters/user_filter_builder.rb +16 -13
  49. data/lib/gooddata/models/user_filters/variable_user_filter.rb +1 -1
  50. data/lib/gooddata/rest/client.rb +4 -2
  51. data/lib/gooddata/rest/connection.rb +37 -30
  52. data/lib/gooddata/rest/connections/rest_client_connection.rb +1 -1
  53. data/lib/gooddata/version.rb +1 -1
  54. data/spec/environment/develop.rb +4 -4
  55. data/spec/environment/hotfix.rb +1 -1
  56. data/spec/environment/release.rb +1 -1
  57. data/spec/integration/full_project_spec.rb +3 -3
  58. data/spec/integration/over_to_user_filters_spec.rb +1 -0
  59. data/spec/integration/project_spec.rb +1 -1
  60. data/spec/integration/user_filters_spec.rb +0 -1
  61. data/spec/unit/commands/command_auth_spec.rb +10 -0
  62. data/spec/unit/extensions/hash_spec.rb +1 -1
  63. data/spec/unit/helpers_spec.rb +0 -8
  64. data/spec/unit/models/domain_spec.rb +1 -9
  65. data/spec/unit/models/from_wire_spec.rb +1 -19
  66. data/spec/unit/models/membership_spec.rb +1 -1
  67. data/spec/unit/models/metadata_spec.rb +1 -1
  68. data/spec/unit/models/profile_spec.rb +23 -47
  69. data/spec/unit/models/schedule_spec.rb +47 -3
  70. metadata +174 -50
  71. data/lib/gooddata/models/from_wire_parse.rb +0 -125
@@ -203,7 +203,11 @@ module GoodData
203
203
  label.find_value_uri(v)
204
204
  end
205
205
  rescue
206
- errors << [label.title, v]
206
+ errors << {
207
+ type: :error,
208
+ label: label.title,
209
+ value: v
210
+ }
207
211
  nil
208
212
  end
209
213
  end
@@ -226,10 +230,10 @@ module GoodData
226
230
  # Encapuslates the creation of filter
227
231
  def self.create_user_filter(expression, related)
228
232
  {
229
- 'related' => related,
230
- 'level' => :user,
231
- 'expression' => expression,
232
- 'type' => :filter
233
+ related: related,
234
+ level: :user,
235
+ expression: expression,
236
+ type: :filter
233
237
  }
234
238
  end
235
239
 
@@ -251,21 +255,21 @@ module GoodData
251
255
  lookups_cache = create_lookups_cache(small_labels)
252
256
  attrs_cache = create_attrs_cache(filters, options)
253
257
 
254
- errors = []
255
- results = filters.pmapcat do |filter|
258
+ results = filters.flat_map do |filter|
256
259
  login = filter[:login]
257
260
  filter[:filters].pmapcat do |f|
258
- expression, error = create_expression(f, labels_cache, lookups_cache, attrs_cache, options)
259
- errors << error unless error.empty?
261
+ expression, errors = create_expression(f, labels_cache, lookups_cache, attrs_cache, options)
260
262
  profiles_uri = (users_cache[login] && users_cache[login].uri)
261
263
  if profiles_uri && expression
262
- [create_user_filter(expression, profiles_uri)]
264
+ [create_user_filter(expression, profiles_uri)] + errors
263
265
  else
264
- []
266
+ [] + errors
265
267
  end
266
268
  end
267
269
  end
268
- [results, errors]
270
+ results.group_by { |i| i[:type] }
271
+ .values_at(:filter, :error)
272
+ .map { |i| i || [] }
269
273
  end
270
274
 
271
275
  def self.resolve_user_filter(user = [], project = [])
@@ -436,7 +440,6 @@ module GoodData
436
440
  end
437
441
 
438
442
  fail "Validation failed #{errors}" if !ignore_missing_values && !errors.empty?
439
-
440
443
  filters = user_filters.map { |data| client.create(klass, data, project: project) }
441
444
  resolve_user_filters(filters, project_filters)
442
445
  end
@@ -9,7 +9,7 @@ module GoodData
9
9
  # @return [String]
10
10
  def save
11
11
  res = client.post(uri, :variable => @json)
12
- @json['uri'] = res['uri']
12
+ @json[:uri] = res['uri']
13
13
  self
14
14
  end
15
15
  end
@@ -67,7 +67,7 @@ module GoodData
67
67
  password = ENV['GD_GEM_PASSWORD']
68
68
  end
69
69
 
70
- username = username.symbolize_keys if username.is_a?(Hash)
70
+ username = GoodData::Helpers.symbolize_keys(username) if username.is_a?(Hash)
71
71
 
72
72
  new_opts = opts.dup
73
73
  if username.is_a?(Hash) && username.key?(:sst_token)
@@ -144,7 +144,7 @@ module GoodData
144
144
  @factory = ObjectFactory.new(self)
145
145
  end
146
146
 
147
- def create_project(options = { title: 'Project', auth_token: ENV['GD_PROJECT_TOKEN'] })
147
+ def create_project(options = { title: 'Project' })
148
148
  GoodData::Project.create({ client: self }.merge(options))
149
149
  end
150
150
 
@@ -246,6 +246,8 @@ module GoodData
246
246
 
247
247
  url = project.links['uploads']
248
248
  fail 'Project WebDAV not supported in this Data Center' unless url
249
+
250
+ GoodData.logger.warn 'Beware! Project webdav is deprecated and should not be used.'
249
251
  url
250
252
  end
251
253
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'terminal-table'
4
4
  require 'securerandom'
5
+ require 'monitor'
6
+ require 'thread_safe'
5
7
 
6
8
  require_relative '../version'
7
9
  require_relative '../exceptions/exceptions'
@@ -10,7 +12,9 @@ module GoodData
10
12
  module Rest
11
13
  # Wrapper of low-level HTTP/REST client/library
12
14
  class Connection
13
- DEFAULT_URL = ENV['GD_SERVER'] || 'https://secure.gooddata.com'
15
+ include MonitorMixin
16
+
17
+ DEFAULT_URL = 'https://secure.gooddata.com'
14
18
  LOGIN_PATH = '/gdc/account/login'
15
19
  TOKEN_PATH = '/gdc/account/token'
16
20
  KEYS_TO_SCRUB = [:password, :verifyPassword, :authorizationToken]
@@ -98,7 +102,8 @@ module GoodData
98
102
  attr_reader :user
99
103
 
100
104
  def initialize(opts)
101
- @stats = {}
105
+ super()
106
+ @stats = ThreadSafe::Hash.new
102
107
 
103
108
  headers = opts[:headers] || {}
104
109
  @webdav_headers = DEFAULT_WEBDAV_HEADERS.merge(headers)
@@ -115,7 +120,7 @@ module GoodData
115
120
 
116
121
  # Connect using username and password
117
122
  def connect(username, password, options = {})
118
- server = options[:server] || DEFAULT_URL
123
+ server = options[:server] || Helpers::AuthHelper.read_server
119
124
  options = DEFAULT_LOGIN_PAYLOAD.merge(options)
120
125
  headers = options[:headers] || {}
121
126
 
@@ -278,10 +283,10 @@ module GoodData
278
283
  # @param uri [String] Target URI
279
284
  def get(uri, options = {}, &user_block)
280
285
  options = log_info(options)
281
- GoodData.logger.debug "GET: #{@server.url}#{uri}"
286
+ GoodData.logger.debug "GET: #{@server.url}#{uri}, #{options}"
282
287
  profile "GET #{uri}" do
283
288
  b = proc do
284
- params = fresh_request_params(options[:request_id])
289
+ params = fresh_request_params(options[:request_id]).merge(options)
285
290
  begin
286
291
  @server[uri].get(params, &user_block)
287
292
  rescue RestClient::Exception => e
@@ -526,7 +531,7 @@ module GoodData
526
531
  def scrub_params(params, keys)
527
532
  keys = keys.reduce([]) { |a, e| a.concat([e.to_s, e.to_sym]) }
528
533
 
529
- new_params = params.deep_dup
534
+ new_params = GoodData::Helpers.deep_dup(params)
530
535
  GoodData::Helpers.hash_dfs(new_params) do |k, _key|
531
536
  keys.each do |key_to_scrub|
532
537
  k[key_to_scrub] = ('*' * k[key_to_scrub].length) if k && k.key?(key_to_scrub) && k[key_to_scrub]
@@ -558,37 +563,39 @@ module GoodData
558
563
  ]
559
564
 
560
565
  def update_stats(title, delta)
561
- orig_title = title
566
+ synchronize do
567
+ orig_title = title
562
568
 
563
- placeholders = true
569
+ placeholders = true
564
570
 
565
- if placeholders
566
- PH_MAP.each do |pm|
567
- break if title.gsub!(pm[1], pm[0])
571
+ if placeholders
572
+ PH_MAP.each do |pm|
573
+ break if title.gsub!(pm[1], pm[0])
574
+ end
568
575
  end
569
- end
570
576
 
571
- stat = stats[title]
572
- if stat.nil?
573
- stat = {
574
- :min => delta,
575
- :max => delta,
576
- :total => 0,
577
- :avg => 0,
578
- :calls => 0,
579
- :entries => []
580
- }
581
- end
577
+ stat = stats[title]
578
+ if stat.nil?
579
+ stat = {
580
+ :min => delta,
581
+ :max => delta,
582
+ :total => 0,
583
+ :avg => 0,
584
+ :calls => 0,
585
+ :entries => []
586
+ }
587
+ end
582
588
 
583
- stat[:min] = delta if delta < stat[:min]
584
- stat[:max] = delta if delta > stat[:max]
585
- stat[:total] += delta
586
- stat[:calls] += 1
587
- stat[:avg] = stat[:total] / stat[:calls]
589
+ stat[:min] = delta if delta < stat[:min]
590
+ stat[:max] = delta if delta > stat[:max]
591
+ stat[:total] += delta
592
+ stat[:calls] += 1
593
+ stat[:avg] = stat[:total] / stat[:calls]
588
594
 
589
- stat[:entries] << orig_title if placeholders
595
+ stat[:entries] << orig_title if placeholders
590
596
 
591
- stats[title] = stat
597
+ stats[title] = stat
598
+ end
592
599
  end
593
600
 
594
601
  def webdav_dir_exists?(url)
@@ -24,7 +24,7 @@ module GoodData
24
24
 
25
25
  # Connect using username and password
26
26
  def connect(username, password, options = {})
27
- server = options[:server] || DEFAULT_URL
27
+ server = options[:server] || Helpers::AuthHelper.read_server
28
28
  @server = RestClient::Resource.new server, DEFAULT_LOGIN_PAYLOAD
29
29
 
30
30
  super
@@ -2,7 +2,7 @@
2
2
 
3
3
  # GoodData Module
4
4
  module GoodData
5
- VERSION = '0.6.19'
5
+ VERSION = '0.6.20'
6
6
 
7
7
  class << self
8
8
  # Version
@@ -8,19 +8,19 @@ module GoodData
8
8
  end
9
9
 
10
10
  module ProcessHelper
11
- PROCESS_ID = '2e2cbe45-02fd-4a1a-b735-a37d65ff267d'
12
- DEPLOY_NAME = 'graph.grf'
11
+ PROCESS_ID = '94e7be67-5f68-405d-bdeb-93006d50482d'
12
+ DEPLOY_NAME = 'graph/graph.grf'
13
13
  end
14
14
 
15
15
  module ProjectHelper
16
- PROJECT_ID = 'i66l5qezxd96syjo9hgbie8earysh6b7'
16
+ PROJECT_ID = 'k8rzngunca3t9dywmxhqzpgwlui3yg0m'
17
17
  PROJECT_URL = "/gdc/projects/#{PROJECT_ID}"
18
18
  PROJECT_TITLE = 'GoodTravis'
19
19
  PROJECT_SUMMARY = 'No summary'
20
20
  end
21
21
 
22
22
  module ScheduleHelper
23
- SCHEDULE_ID = '556c580ee4b05b1a534f3997'
23
+ SCHEDULE_ID = '55953261e4b0a92792febe4e'
24
24
  end
25
25
  end
26
26
  end
@@ -8,7 +8,7 @@ module GoodData
8
8
  end
9
9
 
10
10
  module ProjectHelper
11
- PROJECT_ID = 'vc8mctilky1xu6uqclafvan8b0x1mv3k'
11
+ PROJECT_ID = 'i640il7dyatqmvak24zzr09ypt3ghqu2'
12
12
  PROJECT_URL = "/gdc/projects/#{PROJECT_ID}"
13
13
  PROJECT_TITLE = 'GoodTravis'
14
14
  PROJECT_SUMMARY = 'No summary'
@@ -8,7 +8,7 @@ module GoodData
8
8
  end
9
9
 
10
10
  module ProjectHelper
11
- PROJECT_ID = 'vc8mctilky1xu6uqclafvan8b0x1mv3k'
11
+ PROJECT_ID = 'i640il7dyatqmvak24zzr09ypt3ghqu2'
12
12
  PROJECT_URL = "/gdc/projects/#{PROJECT_ID}"
13
13
  PROJECT_TITLE = 'GoodTravis'
14
14
  PROJECT_SUMMARY = 'No summary'
@@ -260,8 +260,8 @@ describe "Full project implementation", :constraint => 'slow' do
260
260
  end
261
261
 
262
262
  it "should be possible to get all metrics with full objects" do
263
- metrics1 = @project.metrics(:all, full: false)
264
- expect(metrics1.first.class).to be Hash
263
+ metrics = @project.metrics(:all)
264
+ expect(metrics.first.class).to be GoodData::Metric
265
265
  end
266
266
 
267
267
  it "should be able to get a metric by identifier" do
@@ -412,7 +412,7 @@ describe "Full project implementation", :constraint => 'slow' do
412
412
  attribute = @project.attributes('attr.devs.dev_id')
413
413
  label = attribute.primary_label
414
414
  value = label.values.first
415
- different_value = label.values[1]
415
+ different_value = label.values.drop(1).first
416
416
  fact = @project.facts('fact.commits.lines_changed')
417
417
  metric = @project.create_metric("SELECT SUM([#{fact.uri}]) WHERE [#{attribute.uri}] = [#{value[:uri]}]")
418
418
  metric.replace_value(label, value[:value], different_value[:value])
@@ -2,6 +2,7 @@ require 'gooddata'
2
2
 
3
3
  describe "Variables implementation", :constraint => 'slow' do
4
4
  before(:all) do
5
+ GoodData.logging_http_on
5
6
  @spec = JSON.parse(File.read("./spec/data/blueprints/m_n_model.json"), :symbolize_names => true)
6
7
  @client = ConnectionHelper::create_default_connection
7
8
  @blueprint = GoodData::Model::ProjectBlueprint.new(@spec)
@@ -132,7 +132,7 @@ describe GoodData::Project, :constraint => 'slow' do
132
132
 
133
133
  describe '#set_user_roles' do
134
134
  it 'Properly updates user roles as needed' do
135
- users_to_import = @domain.users.sample(5).map {|u| { user: u, role: 'admin' }}
135
+ users_to_import = @domain.users.drop(rand(100)).take(5).map {|u| { user: u, role: 'admin' }}
136
136
  @project.import_users(users_to_import, domain: @domain, whitelists: [/gem_tester@gooddata.com/])
137
137
  users_without_owner = @project.users.reject { |u| u.login == ConnectionHelper::DEFAULT_USERNAME }.pselect { |u| u.role.title == 'Admin' }
138
138
 
@@ -44,7 +44,6 @@ describe "User filters implementation", :constraint => 'slow' do
44
44
  @project.add_data_permissions(filters)
45
45
  metric.execute.should == 6
46
46
  r = @project.compute_report(left: [metric], top: [@label.attribute])
47
- r.include_column?(['tomas@gooddata.com', 1]).should == true
48
47
 
49
48
  r.include_column?(['tomas@gooddata.com', 1]).should == true
50
49
  r.include_column?(['jirka@gooddata.com', 5]).should == true
@@ -11,6 +11,8 @@ describe GoodData::Command::Auth do
11
11
  :email => 'joedoe@example.com',
12
12
  :password => 'secretPassword',
13
13
  :token => 't0k3n1sk0',
14
+ :environment => 'DEVELOPMENT',
15
+ :server => 'https://secure.gooddata.com'
14
16
  }
15
17
 
16
18
  DEFAULT_CREDENTIALS_OVER = {
@@ -59,6 +61,8 @@ describe GoodData::Command::Auth do
59
61
  @input << DEFAULT_CREDENTIALS[:email] << "\n"
60
62
  @input << DEFAULT_CREDENTIALS[:password] << "\n"
61
63
  @input << DEFAULT_CREDENTIALS[:token] << "\n"
64
+ @input << DEFAULT_CREDENTIALS[:environment] << "\n"
65
+ @input << DEFAULT_CREDENTIALS[:server] << "\n"
62
66
  @input.rewind
63
67
 
64
68
  GoodData::Command::Auth.ask_for_credentials
@@ -105,6 +109,8 @@ describe GoodData::Command::Auth do
105
109
  @input << DEFAULT_CREDENTIALS[:email] << "\n"
106
110
  @input << DEFAULT_CREDENTIALS[:password] << "\n"
107
111
  @input << DEFAULT_CREDENTIALS[:token] << "\n"
112
+ @input << DEFAULT_CREDENTIALS[:environment] << "\n"
113
+ @input << DEFAULT_CREDENTIALS[:server] << "\n"
108
114
  @input << 'y' << "\n"
109
115
  @input.rewind
110
116
 
@@ -118,6 +124,8 @@ describe GoodData::Command::Auth do
118
124
  @input << DEFAULT_CREDENTIALS[:email] << "\n"
119
125
  @input << DEFAULT_CREDENTIALS[:password] << "\n"
120
126
  @input << DEFAULT_CREDENTIALS[:token] << "\n"
127
+ @input << DEFAULT_CREDENTIALS[:environment] << "\n"
128
+ @input << DEFAULT_CREDENTIALS[:server] << "\n"
121
129
  @input << 'y' << "\n"
122
130
  @input.rewind
123
131
 
@@ -132,6 +140,8 @@ describe GoodData::Command::Auth do
132
140
  @input << DEFAULT_CREDENTIALS_OVER[:email] << "\n"
133
141
  @input << DEFAULT_CREDENTIALS_OVER[:password] << "\n"
134
142
  @input << DEFAULT_CREDENTIALS_OVER[:token] << "\n"
143
+ @input << DEFAULT_CREDENTIALS[:environment] << "\n"
144
+ @input << DEFAULT_CREDENTIALS[:server] << "\n"
135
145
  @input << 'n' << "\n"
136
146
  @input.rewind
137
147
 
@@ -10,7 +10,7 @@ describe Hash do
10
10
  }
11
11
  }
12
12
  y = x.dup
13
- deep_y = x.deep_dup
13
+ deep_y = GoodData::Helpers.deep_dup(x)
14
14
 
15
15
  y[:a].object_id.should === x[:a].object_id
16
16
  deep_y[:a].object_id.should_not === x[:a].object_id
@@ -37,14 +37,6 @@ describe GoodData::Helpers do
37
37
  end
38
38
  end
39
39
 
40
- describe '#sanitize_string' do
41
- it 'works' do
42
- expect = 'helloworld'
43
- result = GoodData::Helpers.sanitize_string('Hello World')
44
- result.should == expect
45
- end
46
- end
47
-
48
40
  describe "#decode_params" do
49
41
  it 'decodes the data params from json' do
50
42
  params = {
@@ -57,15 +57,7 @@ describe GoodData::Domain do
57
57
  describe '#users' do
58
58
  it 'Should list users' do
59
59
  users = @domain.users
60
- expect(users).to be_instance_of(Array)
61
- users.each do |user|
62
- expect(user).to be_an_instance_of(GoodData::Profile)
63
- end
64
- end
65
-
66
- it 'Accepts pagination options - limit' do
67
- users = @domain.users(:all, limit: 10)
68
- expect(users).to be_instance_of(Array)
60
+ expect(users).to be_instance_of(Enumerator)
69
61
  users.each do |user|
70
62
  expect(user).to be_an_instance_of(GoodData::Profile)
71
63
  end
@@ -287,25 +287,7 @@ describe GoodData::Model::FromWire do
287
287
  expect(GoodData::Model.check_gd_data_type("decimal(10,5)")).to eq true
288
288
  expect(GoodData::Model.check_gd_data_type("decimal(10, 5)")).to eq true
289
289
  end
290
- #
291
- # it "should be able to omit titles if they are superfluous" do
292
- # view = MultiJson.load(File.read('./spec/data/superfluous_titles_view.json'))
293
- # blueprint = FromWire.from_wire(view)
294
- # expect(blueprint.datasets.count).to eq 1
295
- # expect(blueprint.datasets.first.find_column_by_name('current_status', nil).key?(:title)).to eq false
296
- # expect(blueprint.datasets.mapcat { |ds| ds.columns }.any? {|col| col[:name].titleize == col[:title]}).to eq false
297
- # end
298
- #
299
- # it "should enable sorting" do
300
- # skip("UAAA")
301
- # end
302
- #
303
- # it "should generate the same thing it parsed" do
304
- # a = @model_view['projectModelView']['model']['projectModel']['datasets'][3]
305
- # b = @blueprint.to_wire
306
- # # expect(b).to eq a
307
- # end
308
- #
290
+
309
291
  it "should be able to parse description from both attributes and facts" do
310
292
  expect(@blueprint.find_dataset('dataset.opportunity').anchor.description).to eq 'This is opportunity attribute description'
311
293
  expect(@blueprint.find_dataset('dataset.stage_history').facts.find {|f| f.id == 'fact.stage_history.stage_velocity'}.description).to eq 'Velocity description'