gooddata 0.6.0.pre9 → 0.6.0.pre10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. data/.gitignore +3 -0
  2. data/.rspec +3 -0
  3. data/.yardopts +19 -0
  4. data/Gemfile +5 -0
  5. data/README.md +191 -0
  6. data/Rakefile +18 -1
  7. data/bin/gooddata +74 -63
  8. data/doc/.gitignore +1 -0
  9. data/doc/css/.gitkeepme +1 -0
  10. data/doc/images/.gitkeepme +1 -0
  11. data/doc/images/background.png +0 -0
  12. data/doc/images/bg-callout-button.png +0 -0
  13. data/doc/images/header-logo.png +0 -0
  14. data/doc/images/logo-image.png +0 -0
  15. data/doc/js/.gitkeepme +1 -0
  16. data/doc/pages/GET_STARTED.md +309 -0
  17. data/doc/pages/HOMEPAGE.md +75 -0
  18. data/doc/pages/TUTORIALS.md +52 -0
  19. data/doc/pages/tutorial/BRICKS.md +257 -0
  20. data/doc/pages/tutorial/CREATING_A_MODEL.md +79 -0
  21. data/doc/pages/tutorial/CRUNCHING_NUMBERS.md +233 -0
  22. data/doc/pages/tutorial/TEST_DRIVEN_DEVELOPMENT.md +118 -0
  23. data/doc/pages/tutorial/YOUR_FIRST_PROJECT.md +52 -0
  24. data/doc/templates/default/class/dot/setup.rb +6 -0
  25. data/doc/templates/default/class/dot/superklass.erb +3 -0
  26. data/doc/templates/default/class/setup.rb +36 -0
  27. data/doc/templates/default/class/text/setup.rb +11 -0
  28. data/doc/templates/default/class/text/subclasses.erb +5 -0
  29. data/doc/templates/default/constant/text/header.erb +11 -0
  30. data/doc/templates/default/constant/text/setup.rb +3 -0
  31. data/doc/templates/default/docstring/setup.rb +51 -0
  32. data/doc/templates/default/docstring/text/abstract.erb +2 -0
  33. data/doc/templates/default/docstring/text/deprecated.erb +2 -0
  34. data/doc/templates/default/docstring/text/index.erb +2 -0
  35. data/doc/templates/default/docstring/text/note.erb +4 -0
  36. data/doc/templates/default/docstring/text/private.erb +2 -0
  37. data/doc/templates/default/docstring/text/returns_void.erb +1 -0
  38. data/doc/templates/default/docstring/text/text.erb +1 -0
  39. data/doc/templates/default/docstring/text/todo.erb +4 -0
  40. data/doc/templates/default/layout/dot/header.erb +6 -0
  41. data/doc/templates/default/layout/dot/setup.rb +14 -0
  42. data/doc/templates/default/method/setup.rb +3 -0
  43. data/doc/templates/default/method/text/header.erb +1 -0
  44. data/doc/templates/default/method_details/setup.rb +10 -0
  45. data/doc/templates/default/method_details/text/header.erb +10 -0
  46. data/doc/templates/default/method_details/text/method_signature.erb +12 -0
  47. data/doc/templates/default/method_details/text/setup.rb +10 -0
  48. data/doc/templates/default/module/dot/child.erb +1 -0
  49. data/doc/templates/default/module/dot/dependencies.erb +3 -0
  50. data/doc/templates/default/module/dot/header.erb +6 -0
  51. data/doc/templates/default/module/dot/info.erb +14 -0
  52. data/doc/templates/default/module/dot/setup.rb +14 -0
  53. data/doc/templates/default/module/setup.rb +164 -0
  54. data/doc/templates/default/module/text/children.erb +10 -0
  55. data/doc/templates/default/module/text/class_meths_list.erb +8 -0
  56. data/doc/templates/default/module/text/extends.erb +8 -0
  57. data/doc/templates/default/module/text/header.erb +7 -0
  58. data/doc/templates/default/module/text/includes.erb +8 -0
  59. data/doc/templates/default/module/text/instance_meths_list.erb +8 -0
  60. data/doc/templates/default/module/text/setup.rb +12 -0
  61. data/doc/templates/default/root/dot/child.erb +3 -0
  62. data/doc/templates/default/root/dot/setup.rb +5 -0
  63. data/doc/templates/default/tags/setup.rb +55 -0
  64. data/doc/templates/default/tags/text/example.erb +12 -0
  65. data/doc/templates/default/tags/text/index.erb +1 -0
  66. data/doc/templates/default/tags/text/option.erb +20 -0
  67. data/doc/templates/default/tags/text/overload.erb +19 -0
  68. data/doc/templates/default/tags/text/see.erb +11 -0
  69. data/doc/templates/default/tags/text/tag.erb +13 -0
  70. data/examples.rb +2 -2
  71. data/gooddata.gemspec +31 -26
  72. data/lib/gooddata/bricks/middleware/gooddata_middleware.rb +1 -1
  73. data/lib/gooddata/client.rb +65 -53
  74. data/lib/gooddata/commands/commands.rb +9 -0
  75. data/lib/gooddata/commands/process.rb +9 -8
  76. data/lib/gooddata/commands/projects.rb +29 -0
  77. data/lib/gooddata/commands/runners.rb +1 -1
  78. data/lib/gooddata/connection.rb +6 -4
  79. data/lib/gooddata/exceptions.rb +2 -1
  80. data/lib/gooddata/helpers.rb +1 -1
  81. data/lib/gooddata/model.rb +360 -189
  82. data/lib/gooddata/models/metadata.rb +1 -1
  83. data/lib/gooddata/models/metric.rb +2 -1
  84. data/lib/gooddata/models/project.rb +1 -1
  85. data/lib/gooddata/models/report.rb +0 -18
  86. data/lib/gooddata/version.rb +1 -1
  87. data/spec/blueprint_spec.rb +83 -43
  88. data/spec/data/additional_dataset_module.json +18 -0
  89. data/spec/data/blueprint_invalid.json +36 -0
  90. data/spec/data/blueprint_valid.json +37 -0
  91. data/spec/data/model_module.json +18 -0
  92. data/spec/{test_project_model_spec.json → data/test_project_model_spec.json} +4 -0
  93. data/spec/full_project_spec.rb +4 -3
  94. data/spec/helpers/blueprint_helper.rb +17 -0
  95. data/spec/merging_blueprints_spec.rb +23 -48
  96. data/spec/model_dsl_spec.rb +2 -2
  97. data/spec/model_spec.rb +44 -0
  98. data/spec/project_build_and_update_spec.rb +28 -0
  99. data/spec/spec_helper.rb +6 -0
  100. data/yard-server.sh +3 -0
  101. metadata +251 -74
  102. data/README.rdoc +0 -176
@@ -7,7 +7,7 @@ module GoodData::Bricks
7
7
  logger = params[:gdc_logger]
8
8
  token_name = :GDC_SST
9
9
  protocol_name = :GDC_PROTOCOL
10
- server_name = :GDC_SERVER
10
+ server_name = :GDC_HOSTNAME
11
11
  project_id = params[:GDC_PROJECT_ID]
12
12
 
13
13
  fail "SST (SuperSecureToken) not present in params" if params[token_name].nil?
@@ -10,39 +10,47 @@ else
10
10
  FasterCSV = CSV
11
11
  end
12
12
 
13
- # Metadata packages, such as report.rb, require this to be loaded first
14
- require File.dirname(__FILE__) + '/models/metadata.rb'
13
+ # Initializes required dynamically loaded classes
14
+ def init_gd_module()
15
+ # Metadata packages, such as report.rb, require this to be loaded first
16
+ require File.dirname(__FILE__) + '/models/metadata.rb'
15
17
 
16
- Dir[File.dirname(__FILE__) + '/models/*.rb'].each { |file| require file }
17
- Dir[File.dirname(__FILE__) + '/collections/*.rb'].each { |file| require file }
18
+ # Load models from models folder
19
+ Dir[File.dirname(__FILE__) + '/models/*.rb'].each { |file| require file }
18
20
 
19
- # = GoodData API wrapper
21
+ # Load collections
22
+ Dir[File.dirname(__FILE__) + '/collections/*.rb'].each { |file| require file }
23
+ end
24
+
25
+ init_gd_module()
26
+
27
+ # # GoodData API wrapper
20
28
  #
21
29
  # A convenient Ruby wrapper around the GoodData RESTful API.
22
30
  #
23
31
  # The best documentation for the API can be found using these resources:
32
+ #
24
33
  # * http://developer.gooddata.com/api
25
34
  # * https://secure.gooddata.com/gdc
26
35
  #
27
- # == Usage
36
+ # ## Usage
28
37
  #
29
38
  # To communicate with the API you first need a personal GoodData account.
30
- # {Sign up here}[https://secure.gooddata.com/registration.html] if you havent already.
39
+ # [Sign up here](https://secure.gooddata.com/registration.html) if you havent already.
31
40
  #
32
- # Now it is just a matter of initializing the GoodData connection via the connect
33
- # method:
41
+ # Now it is just a matter of initializing the GoodData connection via the connect method:
34
42
  #
35
- # GoodData.connect 'gooddata_user', 'gooddata_password'
43
+ # GoodData.connect 'gooddata_user', 'gooddata_password'
36
44
  #
37
45
  # This GoodData object can now be utalized to retrieve your GoodData profile, the available
38
46
  # projects etc.
39
47
  #
40
- # == Logging
48
+ # ## Logging
41
49
  #
42
- # GoodData.logger = Logger.new(STDOUT)
50
+ # GoodData.logger = Logger.new(STDOUT)
43
51
  #
44
52
  # For details about the logger options and methods, see the
45
- # {Logger module documentation}[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc].
53
+ # (Logger module documentation)[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc].
46
54
  #
47
55
  module GoodData
48
56
  module Threaded
@@ -52,6 +60,7 @@ module GoodData
52
60
  end
53
61
  end
54
62
 
63
+ # Dummy implementation of logger
55
64
  class NilLogger
56
65
  def debug(*args) ; end
57
66
  alias :info :debug
@@ -59,6 +68,7 @@ module GoodData
59
68
  alias :error :debug
60
69
  end
61
70
 
71
+ # Assigns global/default GoodData project
62
72
  def project=(project)
63
73
  GoodData.project = project
64
74
  GoodData.project
@@ -70,21 +80,24 @@ module GoodData
70
80
 
71
81
  RELEASE_INFO_PATH = '/gdc/releaseInfo'
72
82
 
83
+ # Version
73
84
  def version
74
85
  VERSION
75
86
  end
76
87
 
77
- def gem_version_string
88
+ # Identifier of gem version
89
+ # @return Formatted gem version
90
+ def gem_version_string()
78
91
  "gooddata-gem/#{version}"
79
92
  end
80
93
 
81
94
  # Connect to the GoodData API
82
95
  #
83
- # === Parameters
84
- #
85
- # * +user+ - A GoodData username
86
- # * +password+ - A GoodData password
96
+ # @param options
97
+ # @param second_options
98
+ # @param third_options
87
99
  #
100
+ # Goodd
88
101
  def connect(options=nil, second_options=nil, third_options={})
89
102
  if options.is_a? Hash
90
103
  fail "You have to provide login and password" if ((options[:login].nil? || options[:login].empty?) && (options[:password].nil? || options[:password].empty?))
@@ -94,36 +107,34 @@ module GoodData
94
107
  fail "You have to provide login and password" if ((options.nil? || options.empty?) && (second_options.nil? || second_options.empty?))
95
108
  threaded[:connection] = Connection.new(options, second_options, third_options)
96
109
  end
97
-
98
110
  end
99
111
 
112
+ # Turn logging on
100
113
  def logging_on
101
114
  if logger.is_a? NilLogger
102
115
  GoodData::logger = Logger.new(STDOUT)
103
116
  end
104
117
  end
105
118
 
119
+ # Turn logging off
106
120
  def logging_off
107
121
  GoodData::logger = NilLogger.new
108
122
  end
109
123
 
110
124
 
111
125
  # Hepler for starting with SST easier
112
- # === Parameters
113
126
  #
114
- # * +token+ - SST token
115
- # * +options+ - options get routed to connect eventually so everything that you can use there should be possible to use here.
127
+ # @param token SST token
128
+ # @param options Options get routed to connect eventually so everything that you can use there should be possible to use here.
116
129
  #
117
130
  def connect_with_sst(token, options={})
118
131
  create_authenticated_connection(options.merge({:cookies => {"GDCAuthSST" => token}}))
119
132
  end
120
133
 
121
134
  # This method is aimed at creating an authenticated connection in case you do not hae pass/login but you have SST
122
- # === Parameters
123
135
  #
124
- # * +options+ - :server => optional GD server uri. If nil it secure will be used
125
- # * +options+ - :cookies => you can specify a hash of cookies
126
- #
136
+ # @param options :server => optional GD server uri, If nil it secure will be used. :cookies => you can specify a hash of cookies
137
+ #
127
138
  def create_authenticated_connection(options={})
128
139
  connect(options)
129
140
  server_cookies = options[:cookies]
@@ -132,6 +143,10 @@ module GoodData
132
143
  connection
133
144
  end
134
145
 
146
+ # Perform block in context of another project than currently set
147
+ #
148
+ # @param project Project to use
149
+ # @param bl Block to be performed
135
150
  def with_project(project, &bl)
136
151
  fail "You have to specify a project when using with_project" if project.nil? || (project.is_a?(String) && project.empty?)
137
152
  old_project = GoodData.project
@@ -145,24 +160,21 @@ module GoodData
145
160
  end
146
161
  end
147
162
 
148
- # Returns the active GoodData connection earlier initialized via
149
- # GoodData.connect call
163
+ # Returns the active GoodData connection earlier initialized via GoodData.connect call
150
164
  #
151
165
  # @see GoodData.connect
152
- #
153
166
  def connection
154
167
  threaded[:connection] || raise("Please authenticate with GoodData.connect first")
155
168
  end
156
169
 
157
170
  # Sets the active project
158
171
  #
159
- # === Parameters
172
+ # @param project A project identifier
160
173
  #
161
- # * +project+ - a project identifier
174
+ # ### Examples
162
175
  #
163
- # === Examples
176
+ # The following calls are equivalent
164
177
  #
165
- # The following calls are equivalent:
166
178
  # * GoodData.project = 'afawtv356b6usdfsdf34vt'
167
179
  # * GoodData.use 'afawtv356b6usdfsdf34vt'
168
180
  # * GoodData.use '/gdc/projects/afawtv356b6usdfsdf34vt'
@@ -190,12 +202,12 @@ module GoodData
190
202
  #
191
203
  # Retuns the JSON response formatted as a Hash object.
192
204
  #
193
- # === Parameters
205
+ # @param path The HTTP path on the GoodData server (must be prefixed with a forward slash)
206
+ #
207
+ # ### Examples
194
208
  #
195
- # * +path+ - The HTTP path on the GoodData server (must be prefixed with a forward slash)
196
- # === Examples
209
+ # GoodData.get '/gdc/projects'
197
210
  #
198
- # GoodData.get '/gdc/projects'
199
211
  def get(path, options = {})
200
212
  connection.get(path, options)
201
213
  end
@@ -204,14 +216,13 @@ module GoodData
204
216
  #
205
217
  # Retuns the JSON response formatted as a Hash object.
206
218
  #
207
- # === Parameters
208
- #
209
- # * +path+ - The HTTP path on the GoodData server (must be prefixed with a forward slash)
210
- # * +data+ - The payload data in the format of a Hash object
219
+ # @param path The HTTP path on the GoodData server (must be prefixed with a forward slash)
220
+ # @param data The payload data in the format of a Hash object
211
221
  #
212
- # === Examples
222
+ # ### Examples
213
223
  #
214
224
  # GoodData.post '/gdc/projects', { ... }
225
+ #
215
226
  def post(path, data, options = {})
216
227
  connection.post path, data, options
217
228
  end
@@ -220,14 +231,15 @@ module GoodData
220
231
  #
221
232
  # Retuns the JSON response formatted as a Hash object.
222
233
  #
223
- # === Parameters
234
+ # ### Parameters
224
235
  #
225
- # * +path+ - The HTTP path on the GoodData server (must be prefixed with a forward slash)
226
- # * +data+ - The payload data in the format of a Hash object
236
+ # @param path The HTTP path on the GoodData server (must be prefixed with a forward slash)
237
+ # @param data The payload data in the format of a Hash object
227
238
  #
228
- # === Examples
239
+ # ### Examples
229
240
  #
230
241
  # GoodData.put '/gdc/projects', { ... }
242
+ #
231
243
  def put(path, data, options = {})
232
244
  connection.put path, data, options
233
245
  end
@@ -236,13 +248,12 @@ module GoodData
236
248
  #
237
249
  # Retuns the JSON response formatted as a Hash object.
238
250
  #
239
- # === Parameters
251
+ # @param path The HTTP path on the GoodData server (must be prefixed with a forward slash)
240
252
  #
241
- # * +path+ - The HTTP path on the GoodData server (must be prefixed with a forward slash)
253
+ # ### Examples
242
254
  #
243
- # === Examples
255
+ # GoodData.delete '/gdc/project/1'
244
256
  #
245
- # GoodData.delete '/gdc/project/1'
246
257
  def delete(path, options = {})
247
258
  connection.delete path, options
248
259
  end
@@ -306,10 +317,11 @@ module GoodData
306
317
  # For some serious logging, set the logger instance using
307
318
  # the logger= method
308
319
  #
309
- # === Example
320
+ # ### Example
321
+ #
322
+ # require 'logger'
323
+ # GoodData.logger = Logger.new(STDOUT)
310
324
  #
311
- # require 'logger'
312
- # GoodData.logger = Logger.new(STDOUT)
313
325
  def logger
314
326
  @logger ||= NilLogger.new
315
327
  end
@@ -0,0 +1,9 @@
1
+ require './api'
2
+ require './auth'
3
+ require './base'
4
+ require './datasets'
5
+ require './process'
6
+ require './profile'
7
+ require './projects'
8
+ require './runners'
9
+ require './scaffold'
@@ -19,23 +19,25 @@ module GoodData::Command
19
19
  def self.deploy(dir, options={})
20
20
  verbose = options[:verbose] || false
21
21
  GoodData.with_project(options[:project_id]) do
22
- deploy_graph(dir, options.merge({:files_to_exclude => [options[:params]]}))
22
+ params = options[:params].nil? ? [] : [options[:params]]
23
+ deploy_graph(dir, options.merge({:files_to_exclude => params}))
23
24
  end
24
25
  end
25
26
 
26
27
  def self.with_deploy(dir, options={}, &block)
27
28
  verbose = options[:verbose] || false
28
29
  GoodData.with_project(options[:project_id]) do
30
+ params = options[:params].nil? ? [] : [options[:params]]
29
31
  if block
30
32
  begin
31
- res = deploy_graph(dir, options.merge({:files_to_exclude => [options[:params]]}))
33
+ res = deploy_graph(dir, options.merge({:files_to_exclude => params}))
32
34
  block.call(res)
33
35
  ensure
34
- # self_link = res["process"]["links"]["self"]
35
- # GoodData.delete(self_link)
36
+ self_link = res && res["process"]["links"]["self"]
37
+ GoodData.delete(self_link)
36
38
  end
37
39
  else
38
- deploy_graph(dir, options.merge({:files_to_exclude => [options[:params]]}))
40
+ deploy_graph(dir, options.merge({:files_to_exclude => params}))
39
41
  end
40
42
  end
41
43
  end
@@ -100,8 +102,7 @@ module GoodData::Command
100
102
  fail "\"#{dir}\" is not a directory" unless dir.directory?
101
103
  files_to_exclude = options[:files_to_exclude].map {|p| Pathname(p)}
102
104
 
103
- # project_id = options[:project_id] || fail("Project Id has to be specified")
104
-
105
+ project_id = options[:project_id] || fail("Project Id has to be specified")
105
106
 
106
107
  type = options[:type] || "GRAPH"
107
108
  deploy_name = options[:name]
@@ -141,7 +142,7 @@ module GoodData::Command
141
142
  GoodData.put("/gdc/projects/#{GoodData.project.pid}/dataload/processes/#{process_id}", data)
142
143
  end
143
144
  end
144
- puts HighLine::color("Deploy DONE #{dir}", HighLine::BOLD) if verbose
145
+ puts HighLine::color("Deploy DONE #{dir}", HighLine::GREEN) if verbose
145
146
  res
146
147
  end
147
148
 
@@ -66,6 +66,35 @@ module GoodData::Command
66
66
  p.delete
67
67
  end
68
68
 
69
+ def self.get_spec_and_project_id(base_path)
70
+ goodfile_path = GoodData::Helpers.find_goodfile(Pathname(base_path))
71
+ fail "Goodfile could not be located in any parent directory. Please make sure you are inside a gooddata project folder." if goodfile_path.nil?
72
+ goodfile = JSON.parse(File.read(goodfile_path), :symbolize_names => true)
73
+ spec_path = goodfile[:model] || fail("You need to specify the path of the build spec")
74
+ fail "Model path provided in Goodfile \"#{spec_path}\" does not exist" unless File.exist?(spec_path) && !File.directory?(spec_path)
75
+
76
+ spec_path = Pathname(spec_path)
77
+
78
+ content = File.read(spec_path)
79
+ spec = if (spec_path.extname == ".rb")
80
+ eval(content)
81
+ elsif (spec_path.extname == ".json")
82
+ JSON.parse(spec_path, :symbolize_names => true)
83
+ end
84
+ [spec, goodfile[:project_id]]
85
+ end
86
+
87
+ def self.update(options={})
88
+ project = options[:project]
89
+ project_id = project && project.pid
90
+ fail "You have to provide 'project_id'. You can either provide it through -p flag or even better way is to fill it in in your Goodfile under key \"project_id\". If you just started a project you have to create it first. One way might be through \"gooddata project build\"" if project_id.nil? || project_id.empty?
91
+ GoodData::Model::ProjectCreator.migrate(:spec => options[:spec], :project => project_id)
92
+ end
93
+
94
+ def self.build(options={})
95
+ GoodData::Model::ProjectCreator.migrate(:spec => options[:spec], :token => options[:token])
96
+ end
97
+
69
98
  end
70
99
  end
71
100
 
@@ -29,7 +29,7 @@ script_body = <<-script_body
29
29
  :GDC_SST => \"#{sst}\",
30
30
  :GDC_PROJECT_ID => \"#{pid}\",
31
31
  :GDC_PROTOCOL => \"#{scheme}\",
32
- :GDC_SERVER => \"#{hostname}\",
32
+ :GDC_HOSTNAME => \"#{hostname}\",
33
33
  :GDC_LOGGER_FILE => STDOUT
34
34
  }.merge(#{params})
35
35
  eval(File.read(\"./main.rb\"))
@@ -1,7 +1,5 @@
1
1
  require 'json'
2
-
3
- # silence the parenthesis warning in rest-client 1.6.1
4
- old_verbose, $VERBOSE = $VERBOSE, nil ; require 'rest-client' ; $VERBOSE = old_verbose
2
+ require 'rest-client'
5
3
 
6
4
  module GoodData
7
5
 
@@ -368,7 +366,11 @@ module GoodData
368
366
  new_params = Marshal.load(Marshal.dump(params))
369
367
  GoodData::Helpers.hash_dfs(new_params) do |k, key|
370
368
  keys.each do |key_to_scrub|
371
- k[key_to_scrub] = ("*" * k[key_to_scrub].length) if k && k.has_key?(key_to_scrub)
369
+ begin
370
+ k[key_to_scrub] = ("*" * k[key_to_scrub].length) if k && k.has_key?(key_to_scrub) && k[key_to_scrub]
371
+ rescue
372
+ binding.pry
373
+ end
372
374
  end
373
375
  end
374
376
  new_params
@@ -1,5 +1,6 @@
1
1
  module GoodData
2
-
2
+
3
+ # Project Not Found
3
4
  class ProjectNotFound < RestClient::ResourceNotFound
4
5
 
5
6
  end
@@ -26,7 +26,7 @@ module GoodData::Helpers
26
26
  end
27
27
  pwd = pwd.parent
28
28
  end until root == pwd
29
- fail "Goodfile not found in #{pwd.to_s} or any parent up to #{root.to_s}"
29
+ nil
30
30
  end
31
31
 
32
32
  def self.hash_dfs(thing, &block)
@@ -34,8 +34,8 @@ module GoodData
34
34
 
35
35
  class << self
36
36
  def add_dataset(name, columns, project = nil)
37
- Schema.new('columns' => columns, 'name' => name)
38
- add_schema(schema , project)
37
+ Schema.new('columns' => columns, 'name' => name)
38
+ add_schema(schema, project)
39
39
  end
40
40
 
41
41
  def add_schema(schema, project = nil)
@@ -46,7 +46,7 @@ module GoodData
46
46
  project = GoodData.project unless project
47
47
  ldm_links = GoodData.get project.md[LDM_CTG]
48
48
  ldm_uri = Links.new(ldm_links)[LDM_MANAGE_CTG]
49
- GoodData.post ldm_uri, { 'manage' => { 'maql' => schema.to_maql_create } }
49
+ GoodData.post ldm_uri, {'manage' => {'maql' => schema.to_maql_create}}
50
50
  end
51
51
 
52
52
  # Load given file into a data set described by the given schema
@@ -80,7 +80,7 @@ module GoodData
80
80
  end
81
81
 
82
82
  # kick the load
83
- pull = { 'pullIntegration' => File.basename(dir) }
83
+ pull = {'pullIntegration' => File.basename(dir)}
84
84
  link = project.md.links('etl')['pull']
85
85
  task = GoodData.post link, pull
86
86
  while (GoodData.get(task["pullTask"]["uri"])["taskStatus"] === "RUNNING" || GoodData.get(task["pullTask"]["uri"])["taskStatus"] === "PREPARED") do
@@ -92,16 +92,17 @@ module GoodData
92
92
  js = JSON.parse(s.string)
93
93
  fail "Load Failed with error #{JSON.pretty_generate(js)}"
94
94
  end
95
- puts "Done loading"
96
95
  end
97
96
 
98
97
  def merge_dataset_columns(a_schema_blueprint, b_schema_blueprint)
99
- d = Marshal.load(Marshal.dump(a_schema_blueprint))
100
- d[:columns] = d[:columns] + b_schema_blueprint[:columns]
101
- d[:columns].uniq!
102
- columns_that_failed_to_merge = d[:columns].group_by {|x| x[:name]}.map {|k, v| [k, v.count]}.find_all {|x| x[1] > 1}
103
- fail "Columns #{columns_that_failed_to_merge} failed to merge. When merging columns with the same name they have to be identical." unless columns_that_failed_to_merge.empty?
104
- d
98
+ a_schema_blueprint = a_schema_blueprint.to_hash
99
+ b_schema_blueprint = b_schema_blueprint.to_hash
100
+ d = Marshal.load(Marshal.dump(a_schema_blueprint))
101
+ d[:columns] = d[:columns] + b_schema_blueprint[:columns]
102
+ d[:columns].uniq!
103
+ columns_that_failed_to_merge = d[:columns].group_by {|x| x[:name]}.map {|k, v| [k, v.count]}.find_all {|x| x[1] > 1}
104
+ fail "Columns #{columns_that_failed_to_merge} failed to merge. When merging columns with the same name they have to be identical." unless columns_that_failed_to_merge.empty?
105
+ d
105
106
  end
106
107
 
107
108
  end
@@ -110,6 +111,14 @@ module GoodData
110
111
 
111
112
  attr_accessor :data
112
113
 
114
+ def self.from_json(spec)
115
+ if spec.is_a?(String)
116
+ ProjectBlueprint.new(JSON.parse(File.read(spec), :symbolize_names => true))
117
+ else
118
+ ProjectBlueprint.new(spec)
119
+ end
120
+ end
121
+
113
122
  def change(&block)
114
123
  builder = ProjectBuilder.create_from_data(self)
115
124
  block.call(builder)
@@ -119,11 +128,29 @@ module GoodData
119
128
  end
120
129
 
121
130
  def datasets
122
- data[:datasets].map {|d| SchemaBlueprint.new(d)}
131
+ data[:datasets].map { |d| SchemaBlueprint.new(d) }
132
+ end
133
+
134
+ def add_dataset(a_dataset, index=nil)
135
+ if index.nil? || index > datasets.length
136
+ data[:datasets] << a_dataset.to_hash
137
+ else
138
+ data[:datasets].insert(index, a_dataset.to_hash)
139
+ end
140
+ end
141
+
142
+ def remove_dataset(dataset_name)
143
+ x = data[:datasets].find {|d| d[:name] == dataset_name}
144
+ index = data[:datasets].index(x)
145
+ data[:datasets].delete_at(index)
146
+ end
147
+
148
+ def date_dimensions
149
+ data[:date_dimensions]
123
150
  end
124
151
 
125
152
  def get_dataset(name)
126
- ds = data[:datasets].find {|d| d[:name] == name}
153
+ ds = data[:datasets].find { |d| d[:name] == name }
127
154
  SchemaBlueprint.new(ds) unless ds.nil?
128
155
  end
129
156
 
@@ -135,7 +162,7 @@ module GoodData
135
162
  if datasets.count == 1
136
163
  []
137
164
  else
138
- x = datasets.reduce([]) {|memo, schema| schema.has_anchor? ? memo << [schema.name, schema.anchor[:name]] : memo }
165
+ x = datasets.reduce([]) { |memo, schema| schema.has_anchor? ? memo << [schema.name, schema.anchor[:name]] : memo }
139
166
  refs = datasets.reduce([]) do |memo, dataset|
140
167
  memo.concat(dataset.references)
141
168
  end
@@ -147,12 +174,55 @@ module GoodData
147
174
 
148
175
  def model_valid?
149
176
  errors = model_validate
150
- errors.empty? ? true :false
177
+ errors.empty? ? true : false
178
+ end
179
+
180
+ def merge!(a_blueprint)
181
+ temp_blueprint = dup
182
+ a_blueprint.datasets.each do |dataset|
183
+ local_dataset = temp_blueprint.get_dataset(dataset.name)
184
+ if local_dataset.nil?
185
+ temp_blueprint.add_dataset(dataset.dup)
186
+ else
187
+ index = temp_blueprint.datasets.index(local_dataset)
188
+ local_dataset.merge!(dataset)
189
+ temp_blueprint.remove_dataset(local_dataset.name)
190
+ temp_blueprint.add_dataset(local_dataset, index)
191
+ end
192
+ end
193
+ @data = temp_blueprint.data
194
+ self
195
+ end
196
+
197
+ def dup
198
+ deep_copy = Marshal.load(Marshal.dump(data))
199
+ ProjectBlueprint.new(deep_copy)
151
200
  end
152
201
 
153
202
  def title
154
203
  data[:title]
155
204
  end
205
+
206
+ def to_wire_model
207
+ {
208
+ "diffRequest" => {
209
+ "targetModel" => {
210
+ "projectModel" => {
211
+ "datasets" => datasets.map {|d| d.to_wire_model},
212
+ "dateDimensions" => date_dimensions.map {|d|
213
+ {
214
+ "dateDimension" => {
215
+ "name" => d[:name],
216
+ "title" => d[:title] || d[:name].humanize
217
+ }
218
+ }}
219
+ }}}}
220
+ end
221
+
222
+ def to_hash
223
+ @data
224
+ end
225
+
156
226
  end
157
227
 
158
228
  class SchemaBlueprint
@@ -173,10 +243,17 @@ module GoodData
173
243
 
174
244
  def upload(source, options={})
175
245
  project = options[:project] || GoodData.project
246
+ fail "You have to specify a project into which you want to load." if project.nil?
176
247
  mode = options[:load] || "FULL"
177
248
  project.upload(source, to_schema, mode)
178
249
  end
179
250
 
251
+ def merge!(a_blueprint)
252
+ new_blueprint = GoodData::Model.merge_dataset_columns(self, a_blueprint)
253
+ @data = new_blueprint
254
+ self
255
+ end
256
+
180
257
  def name
181
258
  data[:name]
182
259
  end
@@ -194,7 +271,7 @@ module GoodData
194
271
  end
195
272
 
196
273
  def has_anchor?
197
- columns.any? {|c| c[:type] == :anchor}
274
+ columns.any? { |c| c[:type].to_s == "anchor" }
198
275
  end
199
276
 
200
277
  def anchor
@@ -205,19 +282,29 @@ module GoodData
205
282
  find_column_by_type(:reference)
206
283
  end
207
284
 
285
+ def attributes
286
+ find_column_by_type(:attribute)
287
+ end
288
+
289
+ def facts
290
+ find_column_by_type(:fact)
291
+ end
292
+
208
293
  def find_column_by_type(type, all=:all)
294
+ type = type.to_s
209
295
  if all == :all
210
- columns.find_all {|c| c[:type] == type}
296
+ columns.find_all { |c| c[:type].to_s == type }
211
297
  else
212
- columns.find {|c| c[:type] == type}
298
+ columns.find { |c| c[:type].to_s == type }
213
299
  end
214
300
  end
215
301
 
216
302
  def find_column_by_name(type, all=:all)
303
+ type = type.to_s
217
304
  if all == :all
218
- columns.find_all {|c| c[:name] == type}
305
+ columns.find_all { |c| c[:name].to_s == type }
219
306
  else
220
- columns.find {|c| c[:name] == type}
307
+ columns.find { |c| c[:name].to_s == type }
221
308
  end
222
309
  end
223
310
 
@@ -228,12 +315,12 @@ module GoodData
228
315
  def to_manifest
229
316
  to_schema.to_manifest
230
317
  end
231
-
318
+
232
319
  def pretty_print(printer)
233
- printer.text "Schema <#{object_id}>:\n"
320
+ printer.text "Schema <#{object_id}>:\n"
234
321
  printer.text " Name: #{name}\n"
235
322
  printer.text " Columns: \n"
236
- printer.text columns.map {|c| " #{c[:name]}: #{c[:type]}"}.join("\n")
323
+ printer.text columns.map { |c| " #{c[:name]}: #{c[:type]}" }.join("\n")
237
324
  end
238
325
 
239
326
  def dup
@@ -241,6 +328,14 @@ module GoodData
241
328
  SchemaBlueprint.new(deep_copy)
242
329
  end
243
330
 
331
+ def to_wire_model
332
+ to_schema.to_wire_model
333
+ end
334
+
335
+ def ==(other)
336
+ to_hash == other.to_hash
337
+ end
338
+
244
339
  end
245
340
 
246
341
  class ProjectBuilder
@@ -282,8 +377,8 @@ module GoodData
282
377
  def add_dataset(name, &block)
283
378
  builder = GoodData::Model::SchemaBuilder.new(name)
284
379
  block.call(builder)
285
- if @datasets.any? {|item| item[:name] == name}
286
- ds = @datasets.find {|item| item[:name] == name}
380
+ if @datasets.any? { |item| item[:name] == name }
381
+ ds = @datasets.find { |item| item[:name] == name }
287
382
  index = @datasets.index(ds)
288
383
  stuff = GoodData::Model.merge_dataset_columns(ds, builder.to_hash)
289
384
  @datasets.delete_at(index)
@@ -325,9 +420,9 @@ module GoodData
325
420
  mode = options[:mode] || "FULL"
326
421
  dataset = options[:dataset]
327
422
  @uploads << {
328
- :source => data,
329
- :mode => mode,
330
- :dataset => dataset
423
+ :source => data,
424
+ :mode => mode,
425
+ :dataset => dataset
331
426
  }
332
427
  end
333
428
 
@@ -337,9 +432,9 @@ module GoodData
337
432
 
338
433
  def to_json(options={})
339
434
  eliminate_empty = options[:eliminate_empty] || false
340
-
435
+
341
436
  if eliminate_empty
342
- JSON.pretty_generate(to_hash.reject {|k, v| v.is_a?(Enumerable) && v.empty?})
437
+ JSON.pretty_generate(to_hash.reject { |k, v| v.is_a?(Enumerable) && v.empty? })
343
438
  else
344
439
  JSON.pretty_generate(to_hash)
345
440
  end
@@ -347,20 +442,20 @@ module GoodData
347
442
 
348
443
  def to_hash
349
444
  {
350
- :title => @title,
351
- :datasets => @datasets,
352
- :uploads => @uploads,
353
- :dashboards => @dashboards,
354
- :metrics => @metrics,
355
- :reports => @reports,
356
- :users => @users,
357
- :assert_tests => @assert_tests,
358
- :date_dimensions => @date_dimensions
445
+ :title => @title,
446
+ :datasets => @datasets,
447
+ :uploads => @uploads,
448
+ :dashboards => @dashboards,
449
+ :metrics => @metrics,
450
+ :reports => @reports,
451
+ :users => @users,
452
+ :assert_tests => @assert_tests,
453
+ :date_dimensions => @date_dimensions
359
454
  }
360
455
  end
361
456
 
362
457
  def get_dataset(name)
363
- datasets.find {|d| d.name == name}
458
+ datasets.find { |d| d.name == name }
364
459
  end
365
460
 
366
461
  end
@@ -381,8 +476,8 @@ module GoodData
381
476
 
382
477
  def to_hash
383
478
  {
384
- :name => @name,
385
- :tabs => @tabs.map{|tab| tab.to_hash}
479
+ :name => @name,
480
+ :tabs => @tabs.map { |tab| tab.to_hash }
386
481
  }
387
482
  end
388
483
  end
@@ -400,8 +495,8 @@ module GoodData
400
495
 
401
496
  def to_hash
402
497
  {
403
- :title => @title,
404
- :items => @stuff
498
+ :title => @title,
499
+ :items => @stuff
405
500
  }
406
501
  end
407
502
 
@@ -412,7 +507,7 @@ module GoodData
412
507
  attr_accessor :data
413
508
 
414
509
  class << self
415
-
510
+
416
511
  def create_from_data(blueprint)
417
512
  sc = SchemaBuilder.new
418
513
  sc.data = blueprint.to_hash
@@ -423,8 +518,8 @@ module GoodData
423
518
 
424
519
  def initialize(name=nil)
425
520
  @data = {
426
- :name => name,
427
- :columns => []
521
+ :name => name,
522
+ :columns => []
428
523
  }
429
524
  end
430
525
 
@@ -442,31 +537,31 @@ module GoodData
442
537
  end
443
538
 
444
539
  def add_anchor(name, options={})
445
- add_column({ :type => :anchor, :name => name}.merge(options))
540
+ add_column({:type => :anchor, :name => name}.merge(options))
446
541
  self
447
542
  end
448
543
 
449
544
  def add_attribute(name, options={})
450
- add_column({ :type => :attribute, :name => name}.merge(options))
545
+ add_column({:type => :attribute, :name => name}.merge(options))
451
546
  self
452
547
  end
453
548
 
454
549
  def add_fact(name, options={})
455
- add_column({ :type => :fact, :name => name}.merge(options))
550
+ add_column({:type => :fact, :name => name}.merge(options))
456
551
  self
457
552
  end
458
553
 
459
554
  def add_label(name, options={})
460
- add_column({ :type => :label, :name => name}.merge(options))
555
+ add_column({:type => :label, :name => name}.merge(options))
461
556
  self
462
557
  end
463
558
 
464
559
  def add_date(name, options={})
465
- add_column({ :type => :date, :name => name}.merge(options))
560
+ add_column({:type => :date, :name => name}.merge(options))
466
561
  end
467
562
 
468
563
  def add_reference(name, options={})
469
- add_column({ :type => :reference, :name => name}.merge(options))
564
+ add_column({:type => :reference, :name => name}.merge(options))
470
565
  end
471
566
 
472
567
  def to_json
@@ -484,20 +579,21 @@ module GoodData
484
579
  end
485
580
 
486
581
  class ProjectCreator
487
-
582
+
488
583
  class << self
489
584
  def migrate(options={})
490
585
 
491
586
  spec = options[:spec] || fail("You need to provide spec for migration")
492
587
  spec = spec.to_hash
493
-
494
- token = options[:token] || fail("You need to specify token for project creation")
588
+
589
+ token = options[:token]
495
590
  project = options[:project] || GoodData::Project.create(:title => spec[:title], :auth_token => token)
591
+ fail("You need to specify token for project creation") if token.nil? && project.nil?
496
592
 
497
593
  begin
498
594
  GoodData.with_project(project) do |p|
499
- migrate_date_dimensions(p, spec[:date_dimensions] || [])
500
- migrate_datasets(p, spec[:datasets] || [])
595
+ # migrate_date_dimensions(p, spec[:date_dimensions] || [])
596
+ migrate_datasets(p, spec)
501
597
  load(p, spec)
502
598
  migrate_metrics(p, spec[:metrics] || [])
503
599
  migrate_reports(p, spec[:reports] || [])
@@ -516,9 +612,31 @@ module GoodData
516
612
  end
517
613
 
518
614
  def migrate_datasets(project, spec)
519
- spec.each do |ds|
520
- schema = GoodData::Model::Schema.new(ds)
521
- project.add_dataset(schema)
615
+ bp = ProjectBlueprint.new(spec)
616
+ # schema = Schema.load(schema) unless schema.respond_to?(:to_maql_create)
617
+ # project = GoodData.project unless project
618
+ result = GoodData.post("/gdc/projects/#{GoodData.project.pid}/model/diff", bp.to_wire_model)
619
+ link = result["asyncTask"]["link"]["poll"]
620
+ response = GoodData.get(link, :process => false)
621
+ # pp response
622
+ while response.code != 200
623
+ sleep 1
624
+ GoodData.connection.retryable(:tries => 3, :on => RestClient::InternalServerError) do
625
+ sleep 1
626
+ response = GoodData.get(link, :process => false)
627
+ # pp response
628
+ end
629
+ end
630
+ response = GoodData.get(link)
631
+ ldm_links = GoodData.get project.md[LDM_CTG]
632
+ ldm_uri = Links.new(ldm_links)[LDM_MANAGE_CTG]
633
+ chunks = response["projectModelDiff"]["updateScripts"].find_all {|script| script["updateScript"]["preserveData"] == true && script["updateScript"]["cascadeDrops"] == false}.map {|x| x["updateScript"]["maqlDdlChunks"]}.flatten
634
+ chunks.each do |chunk|
635
+ GoodData.post ldm_uri, { 'manage' => { 'maql' => chunk } }
636
+ end
637
+
638
+ bp.datasets.each do |ds|
639
+ schema = ds.to_schema
522
640
  GoodData::ProjectMetadata["manifest_#{schema.name}"] = schema.to_manifest.to_json
523
641
  end
524
642
  end
@@ -549,9 +667,11 @@ module GoodData
549
667
  end
550
668
 
551
669
  def load(project, spec)
552
- spec[:uploads].each do |load|
553
- schema = GoodData::Model::Schema.new(spec[:datasets].detect {|d| d[:name] == load[:dataset]})
554
- project.upload(load[:source], schema, load[:mode])
670
+ if spec.has_key?(:uploads)
671
+ spec[:uploads].each do |load|
672
+ schema = GoodData::Model::Schema.new(spec[:datasets].detect {|d| d[:name] == load[:dataset]})
673
+ project.upload(load[:source], schema, load[:mode])
674
+ end
555
675
  end
556
676
  end
557
677
 
@@ -563,7 +683,7 @@ module GoodData
563
683
  end
564
684
  end
565
685
  end
566
-
686
+
567
687
  class MdObject
568
688
  attr_accessor :name, :title
569
689
 
@@ -590,7 +710,7 @@ module GoodData
590
710
  # model abstractions.
591
711
  #
592
712
  class Schema < MdObject
593
- attr_reader :fields, :attributes, :facts, :folders, :references, :labels, :name, :title
713
+ attr_reader :fields, :attributes, :facts, :folders, :references, :labels, :name, :title, :anchor
594
714
 
595
715
  def self.load(file)
596
716
  Schema.new JSON.load(open(file))
@@ -599,13 +719,13 @@ module GoodData
599
719
  def initialize(config, name = nil)
600
720
  super()
601
721
  @fields = []
602
- @attributes = {}
603
- @facts = {}
722
+ @attributes = []
723
+ @facts = []
604
724
  @folders = {
605
- :facts => {},
606
- :attributes => {}
725
+ :facts => {},
726
+ :attributes => {}
607
727
  }
608
- @references = {}
728
+ @references = []
609
729
  @labels = []
610
730
 
611
731
  config[:name] = name unless config[:name]
@@ -626,7 +746,7 @@ module GoodData
626
746
  when "date"
627
747
  add_date c
628
748
  when "anchor"
629
- set_connection_point c
749
+ set_anchor c
630
750
  when "label"
631
751
  add_label c
632
752
  when "reference"
@@ -635,7 +755,7 @@ module GoodData
635
755
  fail "Unexpected type #{c[:type]} in #{c.inspect}"
636
756
  end
637
757
  end
638
- @connection_point = RecordsOf.new(nil, self) unless @connection_point
758
+ @anchor = Anchor.new(nil, self) unless @anchor
639
759
  end
640
760
 
641
761
  def type_prefix
@@ -654,7 +774,7 @@ module GoodData
654
774
  #
655
775
  def to_maql_drop
656
776
  maql = ""
657
- [ attributes, facts ].each do |obj|
777
+ [attributes, facts].each do |obj|
658
778
  maql += obj.to_maql_drop
659
779
  end
660
780
  maql += "DROP {#{self.identifier}};\n"
@@ -666,7 +786,7 @@ module GoodData
666
786
  def to_maql_create
667
787
  maql = "# Create the '#{self.title}' data set\n"
668
788
  maql += "CREATE DATASET {#{self.identifier}} VISUAL (TITLE \"#{self.title}\");\n\n"
669
- [ attributes, facts, { 1 => @connection_point } ].each do |objects|
789
+ [ attributes, facts, { 1 => @anchor } ].each do |objects|
670
790
  objects.values.each do |obj|
671
791
  maql += "# Create '#{obj.title}' and add it to the '#{self.title}' data set.\n"
672
792
  maql += obj.to_maql_create
@@ -709,26 +829,37 @@ module GoodData
709
829
  #
710
830
  def to_manifest(mode="FULL")
711
831
  {
712
- 'dataSetSLIManifest' => {
713
- 'parts' => fields.reduce([]) { |memo, f| val = f.to_manifest_part(mode); memo << val unless val.nil?; memo },
714
- 'dataSet' => self.identifier,
715
- 'file' => 'data.csv', # should be configurable
716
- 'csvParams' => {
717
- 'quoteChar' => '"',
718
- 'escapeChar' => '"',
719
- 'separatorChar' => ',',
720
- 'endOfLine' => "\n"
832
+ 'dataSetSLIManifest' => {
833
+ 'parts' => fields.reduce([]) { |memo, f| val = f.to_manifest_part(mode); memo << val unless val.nil?; memo },
834
+ 'dataSet' => self.identifier,
835
+ 'file' => 'data.csv', # should be configurable
836
+ 'csvParams' => {
837
+ 'quoteChar' => '"',
838
+ 'escapeChar' => '"',
839
+ 'separatorChar' => ',',
840
+ 'endOfLine' => "\n"
841
+ }
721
842
  }
722
- }
723
843
  }
724
844
  end
725
845
 
846
+ def to_wire_model
847
+ {
848
+ "dataset" => {
849
+ "identifier" => identifier,
850
+ "title" => title,
851
+ "anchor" => @anchor.to_wire_model,
852
+ "facts" => facts.map {|f| f.to_wire_model},
853
+ "attributes" => attributes.map {|a| a.to_wire_model},
854
+ "references" => references.map {|r| r.is_a?(DateReference) ? r.schema_ref : type_prefix + "." + r.schema_ref }}}
855
+ end
856
+
726
857
  private
727
858
 
728
859
  def add_attribute(column)
729
860
  attribute = Attribute.new column, self
730
861
  fields << attribute
731
- add_to_hash(attributes, attribute)
862
+ attributes << attribute
732
863
  add_attribute_folder(attribute.folder)
733
864
  # folders[AttributeFolder.new(attribute.folder)] = 1 if attribute.folder
734
865
  end
@@ -742,7 +873,7 @@ module GoodData
742
873
  def add_fact(column)
743
874
  fact = Fact.new column, self
744
875
  fields << fact
745
- add_to_hash(facts, fact)
876
+ facts << fact
746
877
  add_fact_folder(fact.folder)
747
878
  # folders[FactFolder.new(fact.folder)] = 1 if fact.folder
748
879
  end
@@ -762,24 +893,23 @@ module GoodData
762
893
  def add_reference(column)
763
894
  reference = Reference.new(column, self)
764
895
  fields << reference
765
- add_to_hash(references, reference)
896
+ references << reference
766
897
  end
767
898
 
768
899
  def add_date(column)
769
900
  date = DateColumn.new column, self
770
901
  @fields << date
771
902
  date.parts.values.each { |p| @fields << p }
772
- date.facts.each { |f| add_to_hash(self.facts, f) }
773
- date.attributes.each { |a| add_to_hash(self.attributes, a) }
774
- date.references.each {|r| add_to_hash(self.references, r)}
903
+ date.facts.each { |f| facts << f }
904
+ date.attributes.each { |a| attributes << a }
905
+ date.references.each {|r| references << r}
775
906
  end
776
907
 
777
- def set_connection_point(column)
778
- @connection_point = RecordsOf.new column, self
779
- @fields << @connection_point
908
+ def set_anchor(column)
909
+ @anchor = Anchor.new column, self
910
+ @fields << @anchor
780
911
  end
781
912
 
782
- def add_to_hash(hash, obj); hash[obj.identifier] = obj; end
783
913
  end
784
914
 
785
915
  ##
@@ -792,10 +922,10 @@ module GoodData
792
922
  def initialize(hash, schema)
793
923
  super()
794
924
  raise ArgumentError.new("Schema must be provided, got #{schema.class}") unless schema.is_a? Schema
795
- @name = hash[:name] || raise("Data set fields must have their names defined")
796
- @title = hash[:title] || hash[:name].humanize
797
- @folder = hash[:folder]
798
- @schema = schema
925
+ @name = hash[:name] || raise("Data set fields must have their names defined")
926
+ @title = hash[:title] || hash[:name].humanize
927
+ @folder = hash[:folder]
928
+ @schema = schema
799
929
  end
800
930
 
801
931
  ##
@@ -838,13 +968,19 @@ module GoodData
838
968
  # GoodData attribute abstraction
839
969
  #
840
970
  class Attribute < Column
841
- attr_reader :primary_label
971
+ attr_reader :primary_label, :labels
842
972
 
843
- def type_prefix ; ATTRIBUTE_PREFIX ; end
844
- def folder_prefix; ATTRIBUTE_FOLDER_PREFIX; end
973
+ def type_prefix;
974
+ ATTRIBUTE_PREFIX;
975
+ end
976
+
977
+ def folder_prefix;
978
+ ATTRIBUTE_FOLDER_PREFIX;
979
+ end
845
980
 
846
981
  def initialize(hash, schema)
847
982
  super hash, schema
983
+ @labels = []
848
984
  @primary_label = Label.new hash, self, schema
849
985
  end
850
986
 
@@ -852,7 +988,9 @@ module GoodData
852
988
  @table ||= "d_" + @schema.name + "_" + name
853
989
  end
854
990
 
855
- def key ; "#{@name}#{FK_SUFFIX}" ; end
991
+ def key;
992
+ "#{@name}#{FK_SUFFIX}";
993
+ end
856
994
 
857
995
  def to_maql_create
858
996
  maql = "CREATE ATTRIBUTE {#{identifier}} VISUAL (#{visual})" \
@@ -863,12 +1001,31 @@ module GoodData
863
1001
 
864
1002
  def to_manifest_part(mode)
865
1003
  {
866
- 'referenceKey' => 1,
867
- 'populates' => [ @primary_label.identifier ],
868
- 'mode' => mode,
869
- 'columnName' => name
1004
+ 'referenceKey' => 1,
1005
+ 'populates' => [@primary_label.identifier],
1006
+ 'mode' => mode,
1007
+ 'columnName' => name
1008
+ }
1009
+ end
1010
+
1011
+ def to_wire_model
1012
+ {
1013
+ "attribute" => {
1014
+ "identifier" => identifier,
1015
+ "title" => title,
1016
+ "labels" => labels.map do |l|
1017
+ {
1018
+ "label" => {
1019
+ "identifier" => l.identifier,
1020
+ "title" => l.title,
1021
+ "type" => "GDC.text"
1022
+ }
1023
+ }
1024
+ end
1025
+ }
870
1026
  }
871
1027
  end
1028
+
872
1029
  end
873
1030
 
874
1031
  ##
@@ -877,12 +1034,17 @@ module GoodData
877
1034
  # field
878
1035
  #
879
1036
  class Label < Column
1037
+
1038
+ attr_accessor :attribute
1039
+
880
1040
  def type_prefix ; 'label' ; end
881
1041
 
882
1042
  # def initialize(hash, schema)
883
1043
  def initialize(hash, attribute, schema)
884
1044
  super hash, schema
885
- @attribute = attribute || schema.fields.find {|field| field.name === hash[:reference]}
1045
+ attribute = attribute.nil? ? schema.fields.find {|field| field.name === hash[:reference]} : attribute
1046
+ @attribute = attribute
1047
+ attribute.labels << self
886
1048
  end
887
1049
 
888
1050
  def to_maql_create
@@ -893,9 +1055,9 @@ module GoodData
893
1055
 
894
1056
  def to_manifest_part(mode)
895
1057
  {
896
- 'populates' => [ identifier ],
897
- 'mode' => mode,
898
- 'columnName' => name
1058
+ 'populates' => [identifier],
1059
+ 'mode' => mode,
1060
+ 'columnName' => name
899
1061
  }
900
1062
  end
901
1063
 
@@ -904,6 +1066,7 @@ module GoodData
904
1066
  end
905
1067
 
906
1068
  alias :inspect_orig :inspect
1069
+
907
1070
  def inspect
908
1071
  inspect_orig.sub(/>$/, " @attribute=" + @attribute.to_s.sub(/>$/, " @name=#{@attribute.name}") + '>')
909
1072
  end
@@ -913,15 +1076,14 @@ module GoodData
913
1076
  # A GoodData attribute that represents a data set's connection point or a data set
914
1077
  # without a connection point
915
1078
  #
916
- class RecordsOf < Attribute
1079
+ class Anchor < Attribute
917
1080
  def initialize(column, schema)
918
1081
  if column then
919
1082
  super
920
1083
  else
921
- @name = 'id'
922
- @title = "Records of #{schema.name}"
923
- @folder = nil
924
- @schema = schema
1084
+ super({:type => "anchor", :name => 'id'}, schema)
1085
+ @labels = []
1086
+ @primary_label = nil
925
1087
  end
926
1088
  end
927
1089
 
@@ -938,15 +1100,24 @@ module GoodData
938
1100
  end
939
1101
  maql
940
1102
  end
1103
+
941
1104
  end
942
1105
 
943
1106
  ##
944
1107
  # GoodData fact abstraction
945
1108
  #
946
1109
  class Fact < Column
947
- def type_prefix ; FACT_PREFIX ; end
948
- def column_prefix ; FACT_COLUMN_PREFIX ; end
949
- def folder_prefix; FACT_FOLDER_PREFIX; end
1110
+ def type_prefix;
1111
+ FACT_PREFIX;
1112
+ end
1113
+
1114
+ def column_prefix;
1115
+ FACT_COLUMN_PREFIX;
1116
+ end
1117
+
1118
+ def folder_prefix;
1119
+ FACT_FOLDER_PREFIX;
1120
+ end
950
1121
 
951
1122
  def table
952
1123
  @schema.table
@@ -963,9 +1134,18 @@ module GoodData
963
1134
 
964
1135
  def to_manifest_part(mode)
965
1136
  {
966
- 'populates' => [ identifier ],
967
- 'mode' => mode,
968
- 'columnName' => name
1137
+ 'populates' => [identifier],
1138
+ 'mode' => mode,
1139
+ 'columnName' => name
1140
+ }
1141
+ end
1142
+
1143
+ def to_wire_model
1144
+ {
1145
+ "fact" => {
1146
+ "identifier" => identifier,
1147
+ "title" => title
1148
+ }
969
1149
  }
970
1150
  end
971
1151
  end
@@ -974,13 +1154,16 @@ module GoodData
974
1154
  # Reference to another data set
975
1155
  #
976
1156
  class Reference < Column
1157
+
1158
+ attr_accessor :reference, :schema_ref
1159
+
977
1160
  def initialize(column, schema)
978
1161
  super column, schema
979
1162
  # pp column
980
- @name = column[:name]
981
- @reference = column[:reference]
1163
+ @name = column[:name]
1164
+ @reference = column[:reference]
982
1165
  @schema_ref = column[:dataset]
983
- @schema = schema
1166
+ @schema = schema
984
1167
  end
985
1168
 
986
1169
  ##
@@ -992,7 +1175,9 @@ module GoodData
992
1175
  @identifier ||= "#{ATTRIBUTE_PREFIX}.#{@schema_ref}.#{@reference}"
993
1176
  end
994
1177
 
995
- def key ; "#{@name}_id" ; end
1178
+ def key;
1179
+ "#{@name}_id";
1180
+ end
996
1181
 
997
1182
  def label_column
998
1183
  "#{LABEL_PREFIX}.#{@schema_ref}.#{@reference}"
@@ -1008,52 +1193,14 @@ module GoodData
1008
1193
 
1009
1194
  def to_manifest_part(mode)
1010
1195
  {
1011
- 'populates' => [ label_column ],
1012
- 'mode' => mode,
1013
- 'columnName' => name,
1014
- 'referenceKey' => 1
1196
+ 'populates' => [label_column],
1197
+ 'mode' => mode,
1198
+ 'columnName' => name,
1199
+ 'referenceKey' => 1
1015
1200
  }
1016
1201
  end
1017
1202
  end
1018
1203
 
1019
- ##
1020
- # Fact representation of a date.
1021
- #
1022
- # class DateFact < Fact
1023
- #
1024
- # attr_accessor :format, :output_format
1025
- #
1026
- # def initialize(column, schema)
1027
- # super column, schema
1028
- # @output_format = column["format"] || '("dd/MM/yyyy")'
1029
- # @format = @output_format.gsub('yyyy', '%Y').gsub('MM', '%m').gsub('dd', '%d')
1030
- # end
1031
- #
1032
- # def column_prefix ; DATE_COLUMN_PREFIX ; end
1033
- # def type_prefix ; DATE_FACT_PREFIX ; end
1034
- #
1035
- # def to_csv_header(row)
1036
- # "#{name}_fact"
1037
- # end
1038
- #
1039
- # def to_csv_data(headers, row)
1040
- # val = row[name]
1041
- # val.nil?() ? nil : (Date.strptime(val, format) - BEGINNING_OF_TIMES).to_i
1042
- # rescue ArgumentError
1043
- # raise "Value \"#{val}\" for column \"#{name}\" did not match the format: #{format}. " +
1044
- # "Perhaps you need to add or change the \"format\" key in the data set configuration."
1045
- # end
1046
- #
1047
- # def to_manifest_part(mode)
1048
- # {
1049
- # 'populates' => [ identifier ],
1050
- # 'mode' => mode,
1051
- # 'columnName' => "#{name}_fact"
1052
- # }
1053
- # end
1054
- #
1055
- # end
1056
-
1057
1204
  ##
1058
1205
  # Date as a reference to a date dimension
1059
1206
  #
@@ -1074,11 +1221,11 @@ module GoodData
1074
1221
 
1075
1222
  def to_manifest_part(mode)
1076
1223
  {
1077
- 'populates' => [ "#{identifier}.#{DATE_ATTRIBUTE_DEFAULT_DISPLAY_FORM}" ],
1078
- 'mode' => mode,
1079
- 'constraints' => {"date" => output_format},
1080
- 'columnName' => name,
1081
- 'referenceKey' => 1
1224
+ 'populates' => ["#{identifier}.#{DATE_ATTRIBUTE_DEFAULT_DISPLAY_FORM}"],
1225
+ 'mode' => mode,
1226
+ 'constraints' => {"date" => output_format},
1227
+ 'columnName' => name,
1228
+ 'referenceKey' => 1
1082
1229
  }
1083
1230
  end
1084
1231
 
@@ -1090,21 +1237,22 @@ module GoodData
1090
1237
  # # maql += "INCLUDE TEMPLATE \"#{urn}\" MODIFY (IDENTIFIER \"#{name}\", TITLE \"#{title || name}\");\n"
1091
1238
  # maql += super_maql
1092
1239
  # end
1093
-
1094
1240
  end
1095
1241
 
1096
1242
  ##
1097
1243
  # Date field that's not connected to a date dimension
1098
1244
  #
1099
1245
  class DateAttribute < Attribute
1100
- def key ; "#{DATE_COLUMN_PREFIX}#{super}" ; end
1246
+ def key;
1247
+ "#{DATE_COLUMN_PREFIX}#{super}";
1248
+ end
1101
1249
 
1102
1250
  def to_manifest_part(mode)
1103
1251
  {
1104
- 'populates' => ['label.stuff.mmddyy'],
1105
- "format" => "unknown",
1106
- "mode" => mode,
1107
- "referenceKey" => 1
1252
+ 'populates' => ['label.stuff.mmddyy'],
1253
+ "format" => "unknown",
1254
+ "mode" => mode,
1255
+ "referenceKey" => 1
1108
1256
  }
1109
1257
  end
1110
1258
  end
@@ -1113,8 +1261,13 @@ module GoodData
1113
1261
  # Fact representation of a time of a day
1114
1262
  #
1115
1263
  class TimeFact < Fact
1116
- def column_prefix ; TIME_COLUMN_PREFIX ; end
1117
- def type_prefix ; TIME_FACT_PREFIX ; end
1264
+ def column_prefix;
1265
+ TIME_COLUMN_PREFIX;
1266
+ end
1267
+
1268
+ def type_prefix;
1269
+ TIME_FACT_PREFIX;
1270
+ end
1118
1271
  end
1119
1272
 
1120
1273
  ##
@@ -1128,9 +1281,17 @@ module GoodData
1128
1281
  # Time field that's not connected to a time-of-a-day dimension
1129
1282
  #
1130
1283
  class TimeAttribute < Attribute
1131
- def type_prefix ; TIME_ATTRIBUTE_PREFIX ; end
1132
- def key ; "#{TIME_COLUMN_PREFIX}#{super}" ; end
1133
- def table ; @table ||= "#{super}_tm" ; end
1284
+ def type_prefix;
1285
+ TIME_ATTRIBUTE_PREFIX;
1286
+ end
1287
+
1288
+ def key;
1289
+ "#{TIME_COLUMN_PREFIX}#{super}";
1290
+ end
1291
+
1292
+ def table;
1293
+ @table ||= "#{super}_tm";
1294
+ end
1134
1295
  end
1135
1296
 
1136
1297
  ##
@@ -1143,7 +1304,7 @@ module GoodData
1143
1304
 
1144
1305
  def initialize(column, schema)
1145
1306
  super column, schema
1146
- @parts = {} ; @facts = [] ; @attributes = []; @references = []
1307
+ @parts = {}; @facts = []; @attributes = []; @references = []
1147
1308
 
1148
1309
  # @facts << @parts[:date_fact] = DateFact.new(column, schema)
1149
1310
  if column[:dataset] then
@@ -1204,16 +1365,26 @@ module GoodData
1204
1365
  # GoodData attribute folder abstraction
1205
1366
  #
1206
1367
  class AttributeFolder < Folder
1207
- def type; "ATTRIBUTE"; end
1208
- def type_prefix; "dim"; end
1368
+ def type;
1369
+ "ATTRIBUTE";
1370
+ end
1371
+
1372
+ def type_prefix;
1373
+ "dim";
1374
+ end
1209
1375
  end
1210
1376
 
1211
1377
  ##
1212
1378
  # GoodData fact folder abstraction
1213
1379
  #
1214
1380
  class FactFolder < Folder
1215
- def type; "FACT"; end
1216
- def type_prefix; "ffld"; end
1381
+ def type;
1382
+ "FACT";
1383
+ end
1384
+
1385
+ def type_prefix;
1386
+ "ffld";
1387
+ end
1217
1388
  end
1218
1389
 
1219
1390
  class DateDimension < MdObject