gooddata 0.6.0.pre11 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +12 -1
  3. data/.yardopts +2 -0
  4. data/README.md +6 -3
  5. data/Rakefile +24 -7
  6. data/gooddata +2 -2
  7. data/gooddata.gemspec +4 -3
  8. data/lib/gooddata.rb +17 -12
  9. data/lib/gooddata/bricks/base_downloader.rb +7 -7
  10. data/lib/gooddata/bricks/brick.rb +7 -8
  11. data/lib/gooddata/bricks/bricks.rb +4 -1
  12. data/lib/gooddata/bricks/middleware/base_middleware.rb +2 -2
  13. data/lib/gooddata/bricks/middleware/bench_middleware.rb +5 -6
  14. data/lib/gooddata/bricks/middleware/bulk_salesforce_middleware.rb +21 -22
  15. data/lib/gooddata/bricks/middleware/fs_upload_middleware.rb +3 -4
  16. data/lib/gooddata/bricks/middleware/gooddata_middleware.rb +14 -14
  17. data/lib/gooddata/bricks/middleware/logger_middleware.rb +6 -6
  18. data/lib/gooddata/bricks/middleware/middleware.rb +4 -1
  19. data/lib/gooddata/bricks/middleware/restforce_middleware.rb +29 -32
  20. data/lib/gooddata/bricks/middleware/stdout_middleware.rb +5 -5
  21. data/lib/gooddata/bricks/middleware/twitter_middleware.rb +6 -8
  22. data/lib/gooddata/bricks/utils.rb +3 -3
  23. data/lib/gooddata/cli/cli.rb +4 -2
  24. data/lib/gooddata/cli/commands/api_cmd.rb +6 -4
  25. data/lib/gooddata/cli/commands/auth_cmd.rb +5 -3
  26. data/lib/gooddata/cli/commands/console_cmd.rb +1 -1
  27. data/lib/gooddata/cli/commands/process_cmd.rb +6 -4
  28. data/lib/gooddata/cli/commands/profile_cmd.rb +5 -3
  29. data/lib/gooddata/cli/commands/project_cmd.rb +24 -22
  30. data/lib/gooddata/cli/commands/run_ruby_cmd.rb +12 -10
  31. data/lib/gooddata/cli/commands/scaffold_cmd.rb +8 -6
  32. data/lib/gooddata/cli/hooks.rb +4 -2
  33. data/lib/gooddata/cli/shared.rb +3 -1
  34. data/lib/gooddata/cli/terminal.rb +16 -0
  35. data/lib/gooddata/client.rb +28 -22
  36. data/lib/gooddata/commands/api.rb +43 -26
  37. data/lib/gooddata/commands/auth.rb +22 -53
  38. data/lib/gooddata/commands/base.rb +2 -0
  39. data/lib/gooddata/commands/commands.rb +3 -0
  40. data/lib/gooddata/commands/datasets.rb +39 -136
  41. data/lib/gooddata/commands/process.rb +134 -130
  42. data/lib/gooddata/commands/profile.rb +2 -0
  43. data/lib/gooddata/commands/projects.rb +91 -129
  44. data/lib/gooddata/commands/runners.rb +11 -11
  45. data/lib/gooddata/commands/scaffold.rb +28 -26
  46. data/lib/gooddata/connection.rb +61 -68
  47. data/lib/gooddata/core/core.rb +1 -2
  48. data/lib/gooddata/data/data.rb +7 -0
  49. data/lib/gooddata/data/guesser.rb +114 -0
  50. data/lib/gooddata/exceptions/command_failed.rb +7 -0
  51. data/lib/gooddata/exceptions/exceptions.rb +7 -0
  52. data/lib/gooddata/{exceptions.rb → exceptions/project_not_found.rb} +2 -2
  53. data/lib/gooddata/extensions/big_decimal.rb +5 -0
  54. data/lib/gooddata/extract.rb +2 -0
  55. data/lib/gooddata/goodzilla/goodzilla.rb +11 -12
  56. data/lib/gooddata/helpers.rb +49 -35
  57. data/lib/gooddata/models/attribute.rb +7 -5
  58. data/lib/gooddata/models/dashboard.rb +44 -45
  59. data/lib/gooddata/models/data_result.rb +10 -13
  60. data/lib/gooddata/models/data_set.rb +6 -6
  61. data/lib/gooddata/models/display_form.rb +4 -4
  62. data/lib/gooddata/models/empty_result.rb +4 -3
  63. data/lib/gooddata/models/fact.rb +5 -5
  64. data/lib/gooddata/models/links.rb +3 -1
  65. data/lib/gooddata/models/metadata.rb +34 -32
  66. data/lib/gooddata/models/metric.rb +33 -34
  67. data/lib/gooddata/models/model.rb +165 -173
  68. data/lib/gooddata/models/models.rb +3 -0
  69. data/lib/gooddata/models/process.rb +18 -17
  70. data/lib/gooddata/models/profile.rb +3 -1
  71. data/lib/gooddata/models/project.rb +107 -35
  72. data/lib/gooddata/models/project_metadata.rb +12 -12
  73. data/lib/gooddata/models/report.rb +31 -30
  74. data/lib/gooddata/models/report_data_result.rb +22 -19
  75. data/lib/gooddata/models/report_definition.rb +101 -80
  76. data/lib/gooddata/version.rb +5 -3
  77. data/lib/templates/bricks/brick.rb.erb +3 -3
  78. data/lib/templates/bricks/main.rb.erb +3 -2
  79. data/lib/templates/project/Goodfile.erb +2 -2
  80. data/lib/templates/project/model/model.rb.erb +19 -19
  81. data/spec/data/.gooddata +4 -0
  82. data/spec/helpers/blueprint_helper.rb +2 -2
  83. data/spec/helpers/cli_helper.rb +28 -0
  84. data/spec/helpers/connection_helper.rb +2 -2
  85. data/spec/integration/command_projects_spec.rb +1 -1
  86. data/spec/integration/create_from_template_spec.rb +12 -0
  87. data/spec/integration/full_project_spec.rb +2 -2
  88. data/spec/integration/partial_md_export_import_spec.rb +36 -0
  89. data/spec/logging_in_logging_out_spec.rb +1 -1
  90. data/spec/spec_helper.rb +29 -2
  91. data/spec/unit/cli/cli_spec.rb +3 -3
  92. data/spec/unit/cli/commands/cmd_api_spec.rb +21 -4
  93. data/spec/unit/cli/commands/cmd_auth_spec.rb +2 -4
  94. data/spec/unit/cli/commands/cmd_process_spec.rb +20 -4
  95. data/spec/unit/cli/commands/cmd_profile_spec.rb +9 -4
  96. data/spec/unit/cli/commands/cmd_project_spec.rb +53 -4
  97. data/spec/unit/cli/commands/cmd_run_ruby_spec.rb +2 -4
  98. data/spec/unit/cli/commands/cmd_scaffold_spec.rb +14 -4
  99. data/spec/unit/commands/command_api_spec.rb +21 -2
  100. data/spec/unit/commands/command_auth_spec.rb +62 -1
  101. data/spec/unit/commands/command_dataset_spec.rb +31 -3
  102. data/spec/unit/commands/command_process_spec.rb +75 -1
  103. data/spec/unit/commands/command_profile_spec.rb +7 -1
  104. data/spec/unit/commands/command_projects_spec.rb +1 -1
  105. data/spec/unit/commands/command_scaffold_spec.rb +46 -1
  106. data/spec/unit/core/connection_spec.rb +1 -0
  107. data/spec/unit/data/guesser_spec.rb +54 -0
  108. data/spec/unit/helpers_spec.rb +47 -0
  109. data/spec/unit/model/schema_builder_spec.rb +2 -0
  110. data/spec/unit/model/tools_spec.rb +89 -0
  111. data/test/test_upload.rb +39 -15
  112. metadata +98 -75
  113. data/test/test_commands.rb +0 -85
  114. data/test/test_guessing.rb +0 -46
  115. data/test/test_model.rb +0 -81
  116. data/test/test_rest_api_basic.rb +0 -41
@@ -1,24 +1,25 @@
1
+ # encoding: UTF-8
2
+
1
3
  module GoodData::Command
2
4
  class Runners
3
-
4
5
  def self.run_ruby_locally(brick_dir, options={})
5
6
  pid = options[:project_id]
6
- fail "You have to specify a project ID" if pid.nil?
7
- fail "You have to specify directory of the brick run" if brick_dir.nil?
8
- fail "You specified file as a birck run directory. You have to specify directory." if File.exist?(brick_dir) && !File.directory?(brick_dir)
7
+ fail 'You have to specify a project ID' if pid.nil?
8
+ fail 'You have to specify directory of the brick run' if brick_dir.nil?
9
+ fail 'You specified file as a birck run directory. You have to specify directory.' if File.exist?(brick_dir) && !File.directory?(brick_dir)
9
10
 
10
11
  params = options[:expanded_params] || {}
11
12
 
12
13
  GoodData.connection.connect!
13
- sst = GoodData.connection.cookies[:cookies]["GDCAuthSST"]
14
+ sst = GoodData.connection.cookies[:cookies]['GDCAuthSST']
14
15
  pwd = Pathname.new(Dir.pwd)
15
16
  logger_stream = STDOUT
16
17
 
17
18
  server_uri = URI(options[:server]) unless options[:server].nil?
18
- scheme = server_uri.nil? ? "" : server_uri.scheme
19
- hostname = server_uri.nil? ? "" : server_uri.host
19
+ scheme = server_uri.nil? ? '' : server_uri.scheme
20
+ hostname = server_uri.nil? ? '' : server_uri.host
20
21
 
21
- script_body = <<-script_body
22
+ script_body = <<-script_body
22
23
  require 'fileutils'
23
24
  FileUtils::cd(\"#{pwd+brick_dir}\") do\
24
25
  require 'bundler/setup'
@@ -33,12 +34,11 @@ script_body = <<-script_body
33
34
  }.merge(#{params})
34
35
  eval(File.read(\"./main.rb\"))
35
36
  end
36
- script_body
37
+ script_body
37
38
 
38
39
  Bundler.with_clean_env do
39
- system("ruby", "-e", script_body)
40
+ system('ruby', '-e', script_body)
40
41
  end
41
42
  end
42
-
43
43
  end
44
44
  end
@@ -1,61 +1,63 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'erubis'
4
+ require 'fileutils'
5
+
1
6
  module GoodData::Command
2
7
  class Scaffold
3
- class << self
8
+ TEMPLATES_PATH = Pathname(__FILE__) + '../../../templates'
4
9
 
10
+ class << self
11
+ # Scaffolds new project
12
+ # TODO: Add option for custom output dir
5
13
  def project(name)
6
- require 'erubis'
7
- require 'fileutils'
8
-
9
- templates_path = Pathname(__FILE__) + "../../../templates"
14
+ fail ArgumentError, 'No name specified' if name.nil?
10
15
 
11
16
  FileUtils.mkdir(name)
12
17
  FileUtils.cd(name) do
13
18
 
14
- FileUtils.mkdir("model")
15
- FileUtils.cd("model") do
16
- input = File.read(templates_path + 'project/model/model.rb.erb')
19
+ FileUtils.mkdir('model')
20
+ FileUtils.cd('model') do
21
+ input = File.read(TEMPLATES_PATH + 'project/model/model.rb.erb')
17
22
  eruby = Erubis::Eruby.new(input)
18
- File.open("model.rb", 'w') do |f|
23
+ File.open('model.rb', 'w') do |f|
19
24
  f.write(eruby.result(:name => name))
20
25
  end
21
26
  end
22
27
 
23
- FileUtils.mkdir("data")
24
- FileUtils.cd("data") do
25
- FileUtils.cp(Dir.glob(templates_path + 'project/data/*.csv'), ".")
28
+ FileUtils.mkdir('data')
29
+ FileUtils.cd('data') do
30
+ FileUtils.cp(Dir.glob(TEMPLATES_PATH + 'project/data/*.csv'), '.')
26
31
  end
27
32
 
28
- input = File.read(templates_path + 'project/Goodfile.erb')
33
+ input = File.read(TEMPLATES_PATH + 'project/Goodfile.erb')
29
34
  eruby = Erubis::Eruby.new(input)
30
- File.open("Goodfile", 'w') do |f|
35
+ File.open('Goodfile', 'w') do |f|
31
36
  f.write(eruby.result())
32
37
  end
33
38
  end
34
39
  end
35
40
 
41
+ # Scaffolds new brick
42
+ # TODO: Add option for custom output dir
36
43
  def brick(name)
37
-
38
- require 'erubis'
39
- require 'fileutils'
40
-
41
- templates_path = Pathname(__FILE__) + "../../../templates"
42
-
44
+ fail ArgumentError, 'No name specified' if name.nil?
45
+
43
46
  FileUtils.mkdir(name)
44
47
  FileUtils.cd(name) do
45
- input = File.read(templates_path + 'bricks/brick.rb.erb')
48
+ input = File.read(TEMPLATES_PATH + 'bricks/brick.rb.erb')
46
49
  eruby = Erubis::Eruby.new(input)
47
- File.open("brick.rb", 'w') do |f|
50
+ File.open('brick.rb', 'w') do |f|
48
51
  f.write(eruby.result())
49
52
  end
50
-
51
- input = File.read(templates_path + 'bricks/main.rb.erb')
53
+
54
+ input = File.read(TEMPLATES_PATH + 'bricks/main.rb.erb')
52
55
  eruby = Erubis::Eruby.new(input)
53
- File.open("main.rb", 'w') do |f|
56
+ File.open('main.rb', 'w') do |f|
54
57
  f.write(eruby.result())
55
58
  end
56
59
  end
57
60
  end
58
-
59
61
  end
60
62
  end
61
63
  end
@@ -1,10 +1,11 @@
1
- require 'json'
1
+ # encoding: UTF-8
2
+
3
+ require 'multi_json'
2
4
  require 'rest-client'
3
5
 
4
- require File.join(File.dirname(__FILE__), 'version')
6
+ require_relative 'version'
5
7
 
6
8
  module GoodData
7
-
8
9
  # # GoodData HTTP wrapper
9
10
  #
10
11
  # Provides a convenient HTTP wrapper for talking with the GoodData API.
@@ -27,7 +28,6 @@ module GoodData
27
28
  # To send a HTTP request use either the get, post or delete methods documented below.
28
29
  #
29
30
  class Connection
30
-
31
31
  DEFAULT_URL = 'https://secure.gooddata.com'
32
32
  LOGIN_PATH = '/gdc/account/login'
33
33
  TOKEN_PATH = '/gdc/account/token'
@@ -46,7 +46,7 @@ module GoodData
46
46
  # end
47
47
  #
48
48
  def retryable(options = {}, &block)
49
- opts = { :tries => 1, :on => Exception }.merge(options)
49
+ opts = {:tries => 1, :on => Exception}.merge(options)
50
50
 
51
51
  retry_exception, retries = opts[:on], opts[:tries]
52
52
 
@@ -67,15 +67,14 @@ module GoodData
67
67
  # @param password The GoodData account password
68
68
  #
69
69
  def initialize(username, password, options = {})
70
- @status = :not_connected
71
- @username = username
72
- @password = password
73
- @url = options[:server] || DEFAULT_URL
70
+ @status = :not_connected
71
+ @username = username
72
+ @password = password
73
+ @url = options[:server] || DEFAULT_URL
74
74
  @auth_token = options[:gdc_temporary_token]
75
- @options = options
75
+ @options = options
76
76
 
77
77
  @server = create_server_connection(@url, @options)
78
-
79
78
  end
80
79
 
81
80
  # Returns the user JSON object of the currently logged in GoodData user account.
@@ -158,7 +157,7 @@ module GoodData
158
157
 
159
158
  # Get the cookies associated with the current connection.
160
159
  def cookies
161
- @cookies ||= { :cookies => {} }
160
+ @cookies ||= {:cookies => {}}
162
161
  end
163
162
 
164
163
  # Set the cookies used when communicating with the GoodData API.
@@ -189,13 +188,12 @@ module GoodData
189
188
  # /uploads/ resources are special in that they use a different
190
189
  # host and a basic authentication.
191
190
  def upload(file, options={})
192
-
193
191
  ensure_connection
194
192
 
195
193
  dir = options[:directory] || ''
196
194
  staging_uri = options[:staging_url].to_s
197
195
  url = dir.empty? ? staging_uri : URI.join(staging_uri, "#{dir}/").to_s
198
-
196
+
199
197
  # Make a directory, if needed
200
198
  unless dir.empty? then
201
199
  method = :get
@@ -203,46 +201,46 @@ module GoodData
203
201
  begin
204
202
  # first check if it does exits
205
203
  RestClient::Request.execute({
206
- :method => method,
207
- :url => url,
208
- :timeout => @options[:timeout],
209
- :headers => {
210
- :user_agent => GoodData.gem_version_string
211
- }}.merge(cookies)
204
+ :method => method,
205
+ :url => url,
206
+ :timeout => @options[:timeout],
207
+ :headers => {
208
+ :user_agent => GoodData.gem_version_string
209
+ }}.merge(cookies)
212
210
  )
213
211
  rescue RestClient::Exception => e
214
212
  if e.http_code == 404 then
215
213
  method = :mkcol
216
214
  GoodData.logger.debug "#{method}: #{url}"
217
215
  RestClient::Request.execute({
218
- :method => method,
219
- :url => url,
220
- :timeout => @options[:timeout],
221
- :headers => {
222
- :user_agent => GoodData.gem_version_string
223
- }}.merge(cookies)
216
+ :method => method,
217
+ :url => url,
218
+ :timeout => @options[:timeout],
219
+ :headers => {
220
+ :user_agent => GoodData.gem_version_string
221
+ }}.merge(cookies)
224
222
  )
225
223
  end
226
224
  end
227
225
  end
228
226
 
229
- payload = options[:stream] ? "file" : File.read(file)
230
- filename = options[:filename] || options[:stream] ? "randome-filename.txt" : File.basename(file)
227
+ payload = options[:stream] ? 'file' : File.read(file)
228
+ filename = options[:filename] || options[:stream] ? 'randome-filename.txt' : File.basename(file)
231
229
 
232
230
  # Upload the file
233
231
  # puts "uploading the file #{URI.join(url, filename).to_s}"
234
232
  req = RestClient::Request.new({
235
- :method => :put,
236
- :url => URI.join(url, filename).to_s,
237
- :timeout => @options[:timeout],
238
- :headers => {
239
- :user_agent => GoodData.gem_version_string,
240
- },
241
- :payload => payload,
242
- :raw_response => true,
243
- :user => @username,
244
- :password => @password
245
- })
233
+ :method => :put,
234
+ :url => URI.join(url, filename).to_s,
235
+ :timeout => @options[:timeout],
236
+ :headers => {
237
+ :user_agent => GoodData.gem_version_string,
238
+ },
239
+ :payload => payload,
240
+ :raw_response => true,
241
+ :user => @username,
242
+ :password => @password
243
+ })
246
244
  # .merge(cookies))
247
245
  resp = req.execute
248
246
  true
@@ -252,11 +250,11 @@ module GoodData
252
250
  staging_uri = options[:staging_url].to_s
253
251
  url = staging_uri + what
254
252
  req = RestClient::Request.new({
255
- :method => 'GET',
256
- :url => url,
257
- :user => @username,
258
- :password => @password
259
- })
253
+ :method => 'GET',
254
+ :url => url,
255
+ :user => @username,
256
+ :password => @password
257
+ })
260
258
 
261
259
  if where.is_a?(String)
262
260
  File.open(where, 'w') do |f|
@@ -277,8 +275,8 @@ module GoodData
277
275
  end
278
276
 
279
277
  def disconnect
280
- if connected? && GoodData.connection.user["state"]
281
- GoodData.delete(GoodData.connection.user["state"])
278
+ if connected? && GoodData.connection.user['state']
279
+ GoodData.delete(GoodData.connection.user['state'])
282
280
  @status = :not_connected
283
281
  end
284
282
  end
@@ -287,12 +285,12 @@ module GoodData
287
285
 
288
286
  def create_server_connection(url, options)
289
287
  RestClient::Resource.new url,
290
- :timeout => options[:timeout],
291
- :headers => {
292
- :content_type => :json,
293
- :accept => [ :json, :zip ],
294
- :user_agent => GoodData::gem_version_string,
295
- }
288
+ :timeout => options[:timeout],
289
+ :headers => {
290
+ :content_type => :json,
291
+ :accept => [:json, :zip],
292
+ :user_agent => GoodData::gem_version_string,
293
+ }
296
294
  end
297
295
 
298
296
  def ensure_connection
@@ -300,7 +298,7 @@ module GoodData
300
298
  end
301
299
 
302
300
  def connect
303
- GoodData.logger.info "Connecting to GoodData..."
301
+ GoodData.logger.info 'Connecting to GoodData...'
304
302
  @status = :connecting
305
303
  authenticate
306
304
  end
@@ -313,7 +311,7 @@ module GoodData
313
311
  'remember' => 1
314
312
  }
315
313
  }
316
- GoodData.logger.debug "Logging in..."
314
+ GoodData.logger.debug 'Logging in...'
317
315
  @user = post(LOGIN_PATH, credentials, :dont_reauth => true)['userLogin']
318
316
  refresh_token :dont_reauth => true # avoid infinite loop if refresh_token fails with 401
319
317
 
@@ -334,19 +332,19 @@ module GoodData
334
332
  return response if options[:process] == false
335
333
 
336
334
  if content_type == "application/json" || content_type == "application/json;charset=UTF-8" then
337
- result = response.to_str == '""' ? {} : JSON.parse(response.to_str)
335
+ result = response.to_str == '""' ? {} : MultiJson.load(response.to_str)
338
336
  GoodData.logger.debug "Response: #{result.inspect}"
339
- elsif content_type == "application/zip" then
337
+ elsif content_type == 'application/zip' then
340
338
  result = response
341
- GoodData.logger.debug "Response: a zipped stream"
339
+ GoodData.logger.debug 'Response: a zipped stream'
342
340
  elsif response.headers[:content_length].to_s == '0'
343
341
  result = nil
344
- GoodData.logger.debug "Response: Empty response possibly 204"
342
+ GoodData.logger.debug 'Response: Empty response possibly 204'
345
343
  elsif response.code == 204
346
344
  result = nil
347
- GoodData.logger.debug "Response: 204 no content"
345
+ GoodData.logger.debug 'Response: 204 no content'
348
346
  else
349
- raise "Unsupported response content type '%s':\n%s" % [ content_type, response.to_str[0..127] ]
347
+ raise "Unsupported response content type '%s':\n%s" % [content_type, response.to_str[0..127]]
350
348
  end
351
349
  result
352
350
  rescue RestClient::Exception => e
@@ -356,7 +354,7 @@ module GoodData
356
354
  end
357
355
 
358
356
  def refresh_token(options = {})
359
- GoodData.logger.debug "Getting authentication token..."
357
+ GoodData.logger.debug 'Getting authentication token...'
360
358
  begin
361
359
  get TOKEN_PATH, :dont_reauth => true # avoid infinite loop GET fails with 401
362
360
  rescue RestClient::Unauthorized
@@ -366,20 +364,15 @@ module GoodData
366
364
  end
367
365
 
368
366
  def scrub_params(params, keys)
369
- keys = keys.reduce([]) {|memo, k| memo.concat([k.to_s, k.to_sym])}
367
+ keys = keys.reduce([]) { |memo, k| memo.concat([k.to_s, k.to_sym]) }
370
368
 
371
369
  new_params = Marshal.load(Marshal.dump(params))
372
370
  GoodData::Helpers.hash_dfs(new_params) do |k, key|
373
371
  keys.each do |key_to_scrub|
374
- begin
375
- k[key_to_scrub] = ("*" * k[key_to_scrub].length) if k && k.has_key?(key_to_scrub) && k[key_to_scrub]
376
- rescue
377
- binding.pry
378
- end
372
+ k[key_to_scrub] = ('*' * k[key_to_scrub].length) if k && k.has_key?(key_to_scrub) && k[key_to_scrub]
379
373
  end
380
374
  end
381
375
  new_params
382
376
  end
383
-
384
377
  end
385
378
  end
@@ -1,7 +1,6 @@
1
- # require File.join(File.dirname(__FILE__), "")
1
+ # encoding: UTF-8
2
2
 
3
3
  module GoodData
4
-
5
4
  # Core of GoodData Gem
6
5
  class Core
7
6
  end
@@ -0,0 +1,7 @@
1
+ # encoding: UTF-8
2
+ require 'pathname'
3
+
4
+ base = Pathname(__FILE__).dirname.expand_path
5
+ Dir.glob(base + '*.rb').each do |file|
6
+ require file
7
+ end
@@ -0,0 +1,114 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'date'
4
+
5
+ require_relative '../extract'
6
+ require_relative '../exceptions/command_failed'
7
+
8
+ module GoodData
9
+ module Data
10
+ ##
11
+ # Utility class to guess data types of a data stream by looking at first couple of rows
12
+ #
13
+ class Guesser
14
+ TYPES_PRIORITY = [:connection_point, :fact, :date, :attribute]
15
+ attr_reader :headers
16
+
17
+ class << self
18
+ def sort_types(types)
19
+ types.sort do |x, y|
20
+ TYPES_PRIORITY.index(x) <=> TYPES_PRIORITY.index(y)
21
+ end
22
+ end
23
+ end
24
+
25
+ def initialize(reader)
26
+ @reader = reader
27
+ @headers = reader.shift.map! { |h| h.to_s } or raise 'Empty data set'
28
+ @pros = {}; @cons = {}; @seen = {}
29
+ @headers.map do |h|
30
+ @cons[h.to_s] = {}
31
+ @pros[h.to_s] = {}
32
+ @seen[h.to_s] = {}
33
+ end
34
+ end
35
+
36
+ def guess(limit)
37
+ count = 0
38
+ while row = @reader.shift
39
+ break unless row && !row.empty? && count < limit
40
+ raise '%i fields in row %i, %i expected' % [row.size, count + 1, @headers.size] if row.size != @headers.size
41
+ row.each_with_index do |value, j|
42
+ header = @headers[j]
43
+ number = check_number(header, value)
44
+ date = check_date(header, value)
45
+ store_guess header, {@pros => :attribute} unless number || date
46
+ hash_increment @seen[header], value
47
+ end
48
+ count += 1
49
+ end
50
+ # fields with unique values are connection point candidates
51
+ @seen.each do |header, values|
52
+ store_guess header, {@pros => :connection_point} if values.size == count
53
+ end
54
+ guess_result
55
+ end
56
+
57
+ private
58
+
59
+ def guess_result
60
+ result = {}
61
+ @headers.each do |header|
62
+ result[header] = Guesser::sort_types @pros[header].keys.select { |type| @cons[header][type].nil? }
63
+ end
64
+ result
65
+ end
66
+
67
+ def hash_increment(hash, key)
68
+ if hash[key]
69
+ hash[key] += 1
70
+ else
71
+ hash[key] = 1
72
+ end
73
+ end
74
+
75
+ def check_number(header, value)
76
+ if value.nil? || value =~ /^[\+-]?\d*(\.\d*)?$/
77
+ return store_guess(header, @pros => [:fact, :attribute])
78
+ end
79
+ store_guess header, {@cons => :fact}
80
+ end
81
+
82
+ def check_date(header, value)
83
+ return store_guess(header, @pros => [:date, :attribute, :fact]) if value.nil? || value == '0000-00-00'
84
+ begin
85
+ DateTime.parse value
86
+ return store_guess(header, @pros => [:date, :attribute])
87
+ rescue ArgumentError;
88
+ end
89
+ store_guess header, {@cons => :date}
90
+ end
91
+
92
+ ##
93
+ # Stores a guess about given header.
94
+ #
95
+ # Returns true if the @pros key is present, false otherwise
96
+ #
97
+ # === Parameters
98
+ #
99
+ # * +header+ - A header name
100
+ # * +guess+ - A hash with optional @pros and @cons keys
101
+ #
102
+ def store_guess(header, guess)
103
+ result = !guess[@pros].nil?
104
+ [@pros, @cons].each do |hash|
105
+ if guess[hash] then
106
+ guess[hash] = [guess[hash]] unless guess[hash].is_a? Array
107
+ guess[hash].each { |type| hash_increment hash[header], type }
108
+ end
109
+ end
110
+ result
111
+ end
112
+ end
113
+ end
114
+ end