gooddata 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ module GoodData::Command
2
+ class Profile < Base
3
+ def show
4
+ connect
5
+ pp GoodData.profile.to_json
6
+ end
7
+ alias :index :show
8
+ end
9
+ end
@@ -0,0 +1,51 @@
1
+ module GoodData
2
+ module Command
3
+ class Projects < Base
4
+ def list
5
+ connect
6
+ Project.all.each do |project|
7
+ puts "%s %s" % [project.uri, project.title]
8
+ end
9
+ end
10
+ alias :index :list
11
+
12
+ def create
13
+ connect
14
+
15
+ title = ask "Project name"
16
+ summary = ask "Project summary"
17
+ template = ask "Project template", :default => ''
18
+
19
+ project = Project.create :title => title, :summary => summary, :template => template
20
+
21
+ puts "Project '#{project.title}' with id #{project.uri} created successfully!"
22
+ end
23
+
24
+ def show
25
+ id = args.shift rescue nil
26
+ raise(CommandFailed, "Specify the project key you wish to show.") if id.nil?
27
+ connect
28
+ pp Project[id].to_json
29
+ end
30
+
31
+ def delete
32
+ raise(CommandFailed, "Specify the project key(s) for the project(s) you wish to delete.") if args.size == 0
33
+ connect
34
+ while args.size > 0
35
+ id = args.shift
36
+ project = Project[id]
37
+ ask "Do you want to delete the project '#{project.title}' with id #{project.uri}", :answers => %w(y n) do |answer|
38
+ case answer
39
+ when 'y' then
40
+ puts "Deleting #{project.title}..."
41
+ project.delete
42
+ puts "Project '#{project.title}' with id #{project.uri} deleted successfully!"
43
+ when 'n' then
44
+ puts "Aborting..."
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,7 @@
1
+ module GoodData::Command
2
+ class Version < Base
3
+ def index
4
+ puts GoodData.gem_version_string
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,220 @@
1
+ require 'json/pure'
2
+ require 'net/ftptls'
3
+
4
+ # silence the parenthesis warning in rest-client 1.6.1
5
+ old_verbose, $VERBOSE = $VERBOSE, nil ; require 'rest-client' ; $VERBOSE = old_verbose
6
+
7
+ module GoodData
8
+
9
+ # = GoodData HTTP wrapper
10
+ #
11
+ # Provides a convenient HTTP wrapper for talking with the GoodData API.
12
+ #
13
+ # Remember that the connection is shared amongst the entire application.
14
+ # Therefore you can't be logged in to more than _one_ GoodData account.
15
+ # per session. Simultaneous connections to multiple GoodData accounts is not
16
+ # supported at this time.
17
+ #
18
+ # The GoodData API is a RESTful API that communicates using JSON. This wrapper
19
+ # makes sure that the session is stored between requests and that the JSON is
20
+ # parsed both when sending and receiving.
21
+ #
22
+ # == Usage
23
+ #
24
+ # Before a connection can be made to the GoodData API, you have to supply the user
25
+ # credentials using the set_credentials method:
26
+ #
27
+ # Connection.new(username, password).set_credentials(username, password)
28
+ #
29
+ # To send a HTTP request use either the get, post or delete methods documented below.
30
+ #
31
+ class Connection
32
+
33
+ DEFAULT_URL = 'https://secure.gooddata.com'
34
+ LOGIN_PATH = '/gdc/account/login'
35
+ TOKEN_PATH = '/gdc/account/token'
36
+
37
+ # Set the GoodData account credentials.
38
+ #
39
+ # This have to be performed before any calls to the API.
40
+ #
41
+ # === Parameters
42
+ #
43
+ # * +username+ - The GoodData account username
44
+ # * +password+ - The GoodData account password
45
+ def initialize(username, password, url = nil)
46
+ @status = :not_connected
47
+ @username = username
48
+ @password = password
49
+ @url = url || DEFAULT_URL
50
+ end
51
+
52
+ # Returns the user JSON object of the currently logged in GoodData user account.
53
+ def user
54
+ ensure_connection
55
+ @user
56
+ end
57
+
58
+ # Performs a HTTP GET request.
59
+ #
60
+ # Retuns the JSON response formatted as a Hash object.
61
+ #
62
+ # === Parameters
63
+ #
64
+ # * +path+ - The HTTP path on the GoodData server (must be prefixed with a forward slash)
65
+ #
66
+ # === Examples
67
+ #
68
+ # Connection.new(username, password).get '/gdc/projects'
69
+ def get(path, options = {})
70
+ GoodData.logger.debug "GET #{path}"
71
+ ensure_connection
72
+ process_response(options) { @server[path].get cookies }
73
+ end
74
+
75
+ # Performs a HTTP POST request.
76
+ #
77
+ # Retuns the JSON response formatted as a Hash object.
78
+ #
79
+ # === Parameters
80
+ #
81
+ # * +path+ - The HTTP path on the GoodData server (must be prefixed with a forward slash)
82
+ # * +data+ - The payload data in the format of a Hash object
83
+ #
84
+ # === Examples
85
+ #
86
+ # Connection.new(username, password).post '/gdc/projects', { ... }
87
+ def post(path, data, options = {})
88
+ payload = data.to_json
89
+ GoodData.logger.debug "POST #{path}, payload: #{payload}"
90
+ ensure_connection
91
+ process_response(options) { @server[path].post payload, cookies }
92
+ end
93
+
94
+ # Performs a HTTP DELETE request.
95
+ #
96
+ # Retuns the JSON response formatted as a Hash object.
97
+ #
98
+ # === Parameters
99
+ #
100
+ # * +path+ - The HTTP path on the GoodData server (must be prefixed with a forward slash)
101
+ #
102
+ # === Examples
103
+ #
104
+ # Connection.new(username, password).delete '/gdc/project/1'
105
+ def delete(path)
106
+ GoodData.logger.debug "DELETE #{path}"
107
+ ensure_connection
108
+ process_response { @server[path].delete cookies }
109
+ end
110
+
111
+ # Get the cookies associated with the current connection.
112
+ def cookies
113
+ @cookies ||= { :cookies => {} }
114
+ end
115
+
116
+ # Set the cookies used when communicating with the GoodData API.
117
+ def merge_cookies!(cookies)
118
+ self.cookies
119
+ @cookies[:cookies].merge! cookies
120
+ end
121
+
122
+ # Returns true if a connection have been established to the GoodData API
123
+ # and the login was successful.
124
+ def logged_in?
125
+ @status == :logged_in
126
+ end
127
+
128
+ # The connection will automatically be established once it's needed, which it
129
+ # usually is when either the user, get, post or delete method is called. If you
130
+ # want to force a connection (or a re-connect) you can use this method.
131
+ def connect!
132
+ connect
133
+ end
134
+
135
+ # Uploads a file to GoodData server via FTPS
136
+ def upload(file, dir = nil)
137
+ Net::FTPTLS.open('secure-di.gooddata.com', @username, @password) do |ftp|
138
+ ftp.passive = true
139
+ if dir then
140
+ begin ; ftp.mkdir dir ; rescue ; ensure ; ftp.chdir dir ; end
141
+ end
142
+ ftp.binary = true
143
+ ftp.put file
144
+ end
145
+ end
146
+
147
+ private
148
+
149
+ def ensure_connection
150
+ connect if @status == :not_connected
151
+ end
152
+
153
+ def connect
154
+ # GoodData.logger.info "Connecting to GoodData..."
155
+ @status = :connecting
156
+ authenticate
157
+ end
158
+
159
+ def authenticate
160
+ credentials = {
161
+ 'postUserLogin' => {
162
+ 'login' => @username,
163
+ 'password' => @password,
164
+ 'remember' => 1
165
+ }
166
+ }
167
+
168
+ @server = RestClient::Resource.new @url, :headers => {
169
+ :content_type => :json,
170
+ :accept => [ :json, :zip ],
171
+ :user_agent => GoodData.gem_version_string
172
+ }
173
+
174
+ GoodData.logger.debug "Logging in..."
175
+ @user = post(LOGIN_PATH, credentials, :dont_reauth => true)['userLogin']
176
+ refresh_token :dont_reauth => true # avoid infinite loop if refresh_token fails with 401
177
+
178
+ @status = :logged_in
179
+ end
180
+
181
+ def process_response(options = {})
182
+ begin
183
+ begin
184
+ response = yield
185
+ rescue RestClient::Unauthorized
186
+ raise $! if options[:dont_reauth]
187
+ refresh_token
188
+ response = yield
189
+ end
190
+ merge_cookies! response.cookies
191
+ content_type = response.headers[:content_type]
192
+ if content_type == "application/json" then
193
+ result = response.to_str == '""' ? {} : JSON.parse(response.to_str)
194
+ GoodData.logger.debug "Response: #{result.inspect}"
195
+ elsif content_type == "application/zip" then
196
+ result = response
197
+ GoodData.logger.debug "Response: a zipped stream"
198
+ elsif response.headers[:content_length].to_s == '0'
199
+ result = nil
200
+ else
201
+ raise "Unsupported response content type '%s':\n%s" % [ content_type, response.to_str[0..127] ]
202
+ end
203
+ result
204
+ rescue RestClient::Exception => e
205
+ GoodData.logger.debug "Response: #{e.response}"
206
+ raise $!
207
+ end
208
+ end
209
+
210
+ def refresh_token(options = {})
211
+ GoodData.logger.debug "Getting authentication token..."
212
+ begin
213
+ get TOKEN_PATH, :dont_reauth => true # avoid infinite loop GET fails with 401
214
+ rescue RestClient::Unauthorized
215
+ raise $! if options[:dont_reauth]
216
+ authenticate
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,13 @@
1
+ require 'csv'
2
+
3
+ module GoodData::Extract
4
+ class CsvFile
5
+ def initialize(file)
6
+ @file = file
7
+ end
8
+
9
+ def read(&block)
10
+ CSV.open @file, 'r', &block
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ module GoodData::Helpers
2
+ def home_directory
3
+ running_on_windows? ? ENV['USERPROFILE'] : ENV['HOME']
4
+ end
5
+
6
+ def running_on_windows?
7
+ RUBY_PLATFORM =~ /mswin32|mingw32/
8
+ end
9
+
10
+ def running_on_a_mac?
11
+ RUBY_PLATFORM =~ /-darwin\d/
12
+ end
13
+
14
+ def error(msg)
15
+ STDERR.puts(msg)
16
+ exit 1
17
+ end
18
+ end
@@ -0,0 +1,558 @@
1
+ require 'iconv'
2
+ require 'fastercsv'
3
+
4
+ ##
5
+ # Module containing classes that counter-part GoodData server-side meta-data
6
+ # elements, including the server-side data model.
7
+ #
8
+ module GoodData
9
+ module Model
10
+ # GoodData REST API categories
11
+ LDM_CTG = 'ldm'
12
+ LDM_MANAGE_CTG = 'ldm-manage'
13
+
14
+ # Model naming conventions
15
+ FIELD_PK = 'id'
16
+ FK_SUFFIX = '_id'
17
+ FACT_COLUMN_PREFIX = 'f_'
18
+ DATE_COLUMN_PREFIX = 'dt_'
19
+ TIME_COLUMN_PREFIX = 'tm_'
20
+ LABEL_COLUMN_PREFIX = 'nm_'
21
+ ATTRIBUTE_FOLDER_PREFIX = 'dim'
22
+ ATTRIBUTE_PREFIX = 'attr'
23
+ FACT_PREFIX = 'fact'
24
+ DATE_FACT_PREFIX = 'dt'
25
+ TIME_FACT_PREFIX = 'tm.dt'
26
+ TIME_ATTRIBUTE_PREFIX = 'attr.time'
27
+ FACT_FOLDER_PREFIX = 'ffld'
28
+
29
+ class << self
30
+ def add_dataset(title, columns, project = nil)
31
+ add_schema Schema.new('columns' => columns, 'title' => title), project
32
+ end
33
+
34
+ def add_schema(schema, project = nil)
35
+ unless schema.is_a?(Schema) || schema.is_a?(String) then
36
+ raise ArgumentError.new("Schema object or schema file path expected, got '#{schema}'")
37
+ end
38
+ schema = Schema.load schema unless schema.is_a? Schema
39
+ project = GoodData.project unless project
40
+ ldm_links = GoodData.get project.md[LDM_CTG]
41
+ ldm_uri = Links.new(ldm_links)[LDM_MANAGE_CTG]
42
+ GoodData.post ldm_uri, { 'manage' => { 'maql' => schema.to_maql_create } }
43
+ end
44
+
45
+ def to_id(str)
46
+ Iconv.iconv('ascii//ignore//translit', 'utf-8', str) \
47
+ .to_s.gsub(/[^\w\d_]/, '').gsub(/^[\d_]*/, '').downcase
48
+ end
49
+ end
50
+
51
+ class MdObject
52
+ attr_accessor :name, :title
53
+
54
+ def visual
55
+ "TITLE \"#{title_esc}\""
56
+ end
57
+
58
+ def title_esc
59
+ title.gsub(/"/, "\\\"")
60
+ end
61
+
62
+ ##
63
+ # Generates an identifier from the object name by transliterating
64
+ # non-Latin character and then dropping non-alphanumerical characters.
65
+ #
66
+ def identifier
67
+ @identifier ||= "#{self.type_prefix}.#{Model::to_id(name)}"
68
+ end
69
+ end
70
+
71
+ ##
72
+ # Server-side representation of a local data set; includes connection point,
73
+ # attributes and labels, facts, folders and corresponding pieces of physical
74
+ # model abstractions.
75
+ #
76
+ class Schema < MdObject
77
+ attr_reader :fields
78
+
79
+ def self.load(file)
80
+ Schema.new JSON.load(open(file))
81
+ end
82
+
83
+ def initialize(config, title = nil)
84
+ @fields = []
85
+ config['title'] = title unless config['title']
86
+ raise 'Schema name not specified' unless config['title']
87
+ self.title = config['title']
88
+ self.config = config
89
+ end
90
+
91
+ def config=(config)
92
+ labels = []
93
+ config['columns'].each do |c|
94
+ add_attribute c if c['type'] == 'ATTRIBUTE'
95
+ add_fact c if c['type'] == 'FACT'
96
+ add_date c if c['type'] == 'DATE'
97
+ set_connection_point c if c['type'] == 'CONNECTION_POINT'
98
+ labels.push c if c['type'] == 'LABEL'
99
+ end
100
+ @connection_point = RecordsOf.new(nil, self) unless @connection_point
101
+ end
102
+
103
+ def title=(title)
104
+ @name = title
105
+ @title = title
106
+ end
107
+
108
+ def type_prefix ; 'dataset' ; end
109
+
110
+ def attributes; @attributes ||= {} ; end
111
+ def facts; @facts ||= {} ; end
112
+ def folders; @folders ||= {}; end
113
+
114
+ ##
115
+ # Underlying fact table name
116
+ #
117
+ def table
118
+ @table ||= FACT_COLUMN_PREFIX + Model::to_id(name)
119
+ end
120
+
121
+ ##
122
+ # Generates MAQL DDL script to drop this data set and included pieces
123
+ #
124
+ def to_maql_drop
125
+ maql = ""
126
+ [ attributes, facts ].each do |obj|
127
+ maql += obj.to_maql_drop
128
+ end
129
+ maql += "DROP {#{self.identifier}};\n"
130
+ end
131
+
132
+ ##
133
+ # Generates MAQL DDL script to create this data set and included pieces
134
+ #
135
+ def to_maql_create
136
+ maql = "# Create the '#{self.title}' data set\n"
137
+ maql += "CREATE DATASET {#{self.identifier}} VISUAL (TITLE \"#{self.title}\");\n\n"
138
+ [ attributes, facts, { 1 => @connection_point } ].each do |objects|
139
+ objects.values.each do |obj|
140
+ maql += "# Create '#{obj.title}' and add it to the '#{self.title}' data set.\n"
141
+ maql += obj.to_maql_create
142
+ maql += "ALTER DATASET {#{self.identifier}} ADD {#{obj.identifier}};\n\n"
143
+ end
144
+ end
145
+ folders_maql = "# Create folders\n"
146
+ folders.keys.each { |folder| folders_maql += folder.to_maql_create }
147
+ folders_maql + "\n" + maql + "SYNCHRONIZE {#{identifier}};\n"
148
+ end
149
+
150
+ # Load given file into a data set described by the given schema
151
+ #
152
+ def upload(path, project = nil)
153
+ path = path.path if path.respond_to? :path
154
+ project = GoodData.project unless project
155
+
156
+ # create a temporary zip file
157
+ dir = Dir.mktmpdir
158
+ Zip::ZipFile.open("#{dir}/upload.zip", Zip::ZipFile::CREATE) do |zip|
159
+ # TODO make sure schema columns match CSV column names
160
+ zip.get_output_stream('upload_info.json') { |f| f.puts JSON.pretty_generate(to_manifest) }
161
+ zip.get_output_stream('data.csv') do |f|
162
+ FasterCSV.foreach(path) { |row| f.puts row.to_csv }
163
+ end
164
+ end
165
+
166
+ # upload it
167
+ GoodData.connection.upload "#{dir}/upload.zip", File.basename(dir)
168
+ FileUtils.rm_rf dir
169
+
170
+ # kick the load
171
+ pull = { 'pullIntegration' => File.basename(dir) }
172
+ link = project.md.links('etl')['pull']
173
+ GoodData.post link, pull
174
+ end
175
+
176
+ # Generates the SLI manifest describing the data loading
177
+ #
178
+ def to_manifest
179
+ {
180
+ 'dataSetSLIManifest' => {
181
+ 'parts' => fields.map { |f| f.to_manifest_part },
182
+ 'dataSet' => self.identifier,
183
+ 'file' => 'data.csv', # should be configurable
184
+ 'csvParams' => {
185
+ 'quoteChar' => '"',
186
+ 'escapeChar' => '"',
187
+ 'separatorChar' => ',',
188
+ 'endOfLine' => "\n"
189
+ }
190
+ }
191
+ }
192
+ end
193
+
194
+ private
195
+
196
+ def add_attribute(column)
197
+ attribute = Attribute.new column, self
198
+ @fields << attribute
199
+ add_to_hash(self.attributes, attribute)
200
+ folders[AttributeFolder.new(attribute.folder)] = 1 if attribute.folder
201
+ end
202
+
203
+ def add_fact(column)
204
+ fact = Fact.new column, self
205
+ @fields << fact
206
+ add_to_hash(self.facts, fact)
207
+ folders[FactFolder.new(fact.folder)] = 1 if fact.folder
208
+ end
209
+
210
+ def add_date(column)
211
+ date = DateColumn.new column, self
212
+ date.parts.values.each { |p| @fields << p }
213
+ date.facts.each { |f| add_to_hash(self.facts, f) }
214
+ date.attributes.each { |a| add_to_hash(self.attributes, a) }
215
+ @fields << date
216
+ end
217
+
218
+ def set_connection_point(column)
219
+ @connection_point = RecordsOf.new column, self
220
+ @fields << @connection_point
221
+ end
222
+
223
+ def add_to_hash(hash, obj); hash[obj.identifier] = obj; end
224
+ end
225
+
226
+ ##
227
+ # This is a base class for server-side LDM elements such as attributes, labels and
228
+ # facts
229
+ #
230
+ class Column < MdObject
231
+ attr_accessor :folder, :name, :title, :schema
232
+
233
+ def initialize(hash, schema)
234
+ raise ArgumentError.new("Schema must be provided, got #{schema.class}") unless schema.is_a? Schema
235
+ @name = hash['name'] || raise("Data set fields must have their names defined")
236
+ @title = hash['title'] || hash['name']
237
+ @folder = hash['folder']
238
+ @schema = schema
239
+ end
240
+
241
+ ##
242
+ # Generates an identifier from the object name by transliterating
243
+ # non-Latin character and then dropping non-alphanumerical characters.
244
+ #
245
+ def identifier
246
+ @identifier ||= "#{self.type_prefix}.#{Model::to_id @schema.title}.#{Model::to_id name}"
247
+ end
248
+
249
+ def to_maql_drop
250
+ "DROP {#{self.identifier}};\n"
251
+ end
252
+
253
+ def visual
254
+ visual = super
255
+ visual += ", FOLDER {#{folder_prefix}.#{Model::to_id(folder)}}" if folder
256
+ visual
257
+ end
258
+
259
+ # Overriden to prevent long strings caused by the @schema attribute
260
+ #
261
+ def inspect
262
+ to_s.sub(/>$/, " @title=#{@title.inspect}, @name=#{@name.inspect}, @folder=#{@folder.inspect}," \
263
+ " @schema=#{@schema.to_s.sub(/>$/, ' @title=' + @schema.name.inspect + '>')}" \
264
+ ">")
265
+ end
266
+ end
267
+
268
+ ##
269
+ # GoodData attribute abstraction
270
+ #
271
+ class Attribute < Column
272
+ attr_reader :primary_label
273
+
274
+ def type_prefix ; ATTRIBUTE_PREFIX ; end
275
+ def folder_prefix; ATTRIBUTE_FOLDER_PREFIX; end
276
+
277
+ def initialize(hash, schema)
278
+ super hash, schema
279
+ @primary_label = Label.new hash, self, schema
280
+ end
281
+
282
+ def table
283
+ @table ||= "d_" + Model::to_id(@schema.name) + "_" + Model::to_id(name)
284
+ end
285
+
286
+ def key ; "#{Model::to_id(@name)}#{FK_SUFFIX}" ; end
287
+
288
+ def to_maql_create
289
+ "CREATE ATTRIBUTE {#{identifier}} VISUAL (#{visual})" \
290
+ + " AS KEYS {#{table}.#{Model::FIELD_PK}} FULLSET;\n" \
291
+ + @primary_label.to_maql_create
292
+ end
293
+
294
+ def to_manifest_part
295
+ {
296
+ 'referenceKey' => 1,
297
+ 'populates' => [ @primary_label.identifier ],
298
+ 'mode' => 'FULL',
299
+ 'columnName' => name
300
+ }
301
+ end
302
+ end
303
+
304
+ ##
305
+ # GoodData display form abstraction. Represents a default representation
306
+ # of an attribute column or an additional representation defined in a LABEL
307
+ # field
308
+ #
309
+ class Label < Column
310
+ def type_prefix ; 'label' ; end
311
+
312
+ def initialize(hash, attribute, schema)
313
+ super hash, schema
314
+ @attribute = attribute
315
+ end
316
+
317
+ def to_maql_create
318
+ "ALTER ATTRIBUTE {#{@attribute.identifier}} ADD LABELS {#{identifier}}" \
319
+ + " VISUAL (TITLE #{title.inspect}) AS {#{column}};\n"
320
+ end
321
+
322
+ def to_manifest_part
323
+ {
324
+ 'populates' => [ identifier ],
325
+ 'mode' => 'FULL',
326
+ 'columnName' => name
327
+ }
328
+ end
329
+
330
+ def column
331
+ "#{@attribute.table}.#{LABEL_COLUMN_PREFIX}#{Model::to_id name}"
332
+ end
333
+
334
+ alias :inspect_orig :inspect
335
+ def inspect
336
+ inspect_orig.sub(/>$/, " @attribute=" + @attribute.to_s.sub(/>$/, " @name=#{@attribute.name}") + '>')
337
+ end
338
+ end
339
+
340
+ ##
341
+ # A GoodData attribute that represents a data set's connection point or a data set
342
+ # without a connection point
343
+ #
344
+ class RecordsOf < Attribute
345
+ def initialize(column, schema)
346
+ if column then
347
+ super
348
+ else
349
+ @name = 'id'
350
+ @title = "Records of #{schema.name}"
351
+ @folder = nil
352
+ @schema = schema
353
+ end
354
+ end
355
+
356
+ def table
357
+ @table ||= "f_" + Model::to_id(@schema.name)
358
+ end
359
+
360
+ def to_maql_create
361
+ maql = super
362
+ maql += "\n# Connect '#{self.title}' to all attributes of this data set\n"
363
+ @schema.attributes.values.each do |c|
364
+ maql += "ALTER ATTRIBUTE {#{c.identifier}} ADD KEYS " \
365
+ + "{#{table}.#{c.key}};\n"
366
+ end
367
+ maql
368
+ end
369
+ end
370
+
371
+ ##
372
+ # GoodData fact abstraction
373
+ #
374
+ class Fact < Column
375
+ def type_prefix ; FACT_PREFIX ; end
376
+ def column_prefix ; FACT_COLUMN_PREFIX ; end
377
+ def folder_prefix; FACT_FOLDER_PREFIX; end
378
+
379
+ def table
380
+ @schema.table
381
+ end
382
+
383
+ def column
384
+ @column ||= table + '.' + column_prefix + Model::to_id(name)
385
+ end
386
+
387
+ def to_maql_create
388
+ "CREATE FACT {#{self.identifier}} VISUAL (#{visual})" \
389
+ + " AS {#{column}};\n"
390
+ end
391
+
392
+ def to_manifest_part
393
+ {
394
+ 'populates' => [ identifier ],
395
+ 'mode' => 'FULL',
396
+ 'columnName' => column
397
+ }
398
+ end
399
+ end
400
+
401
+ ##
402
+ # Reference to another data set
403
+ #
404
+ class Reference
405
+ def initialize(column, schema)
406
+ @name = column['name']
407
+ @reference = column['reference']
408
+ @schema_ref = column['schema_ref']
409
+ @schema = schema
410
+ end
411
+
412
+ ##
413
+ # Generates an identifier of the referencing attribute using the
414
+ # schema name derived from schemaReference and column name derived
415
+ # from the reference key.
416
+ #
417
+ def identifier
418
+ @identifier ||= "#{ATTRIBUTE_PREFIX}.#{Model::to_id @schema_ref.title}.#{Model::to_id @reference}"
419
+ end
420
+
421
+ def key ; "#{Model::to_id @name}_id" ; end
422
+
423
+ def label_column
424
+ @column ||= "#{@schema.table}.#{LABEL_COLUMN_PREFIX Model::to_id(reference)}"
425
+ end
426
+
427
+ def to_maql_create
428
+ "ALTER ATTRIBUTE {#{self.identifier} ADD KEYS {#{@schema.table}.#{key}}"
429
+ end
430
+
431
+ def to_maql_drop
432
+ "ALTER ATTRIBUTE {#{self.identifier} DROP KEYS {#{@schema.table}.#{key}}"
433
+ end
434
+
435
+ def to_manifest_part
436
+ {
437
+ 'populates' => [ identifier ],
438
+ 'mode' => 'FULL',
439
+ 'columnName' => label_column
440
+ }
441
+ end
442
+ end
443
+
444
+ ##
445
+ # Fact representation of a date.
446
+ #
447
+ class DateFact < Fact
448
+ def column_prefix ; DATE_COLUMN_PREFIX ; end
449
+ def type_prefix ; DATE_FACT_PREFIX ; end
450
+ end
451
+
452
+ ##
453
+ # Date as a reference to a date dimension
454
+ #
455
+ class DateReference < Reference
456
+
457
+ end
458
+
459
+ ##
460
+ # Date field that's not connected to a date dimension
461
+ #
462
+ class DateAttribute < Attribute
463
+ def key ; "#{DATE_COLUMN_PREFIX}#{super}" ; end
464
+ end
465
+
466
+ ##
467
+ # Fact representation of a time of a day
468
+ #
469
+ class TimeFact < Fact
470
+ def column_prefix ; TIME_COLUMN_PREFIX ; end
471
+ def type_prefix ; TIME_FACT_PREFIX ; end
472
+ end
473
+
474
+ ##
475
+ # Time as a reference to a time-of-a-day dimension
476
+ #
477
+ class TimeReference < Reference
478
+
479
+ end
480
+
481
+ ##
482
+ # Time field that's not connected to a time-of-a-day dimension
483
+ #
484
+ class TimeAttribute < Attribute
485
+ def type_prefix ; TIME_ATTRIBUTE_PREFIX ; end
486
+ def key ; "#{TIME_COLUMN_PREFIX}#{super}" ; end
487
+ def table ; @table ||= "#{super}_tm" ; end
488
+ end
489
+
490
+ ##
491
+ # Date column. A container holding the following
492
+ # parts: date fact, a date reference or attribute and an optional time component
493
+ # that contains a time fact and a time reference or attribute.
494
+ #
495
+ class DateColumn
496
+ attr_reader :parts, :facts, :attributes
497
+
498
+ def initialize(column, schema)
499
+ @parts = {} ; @facts = [] ; @attributes = []
500
+
501
+ @facts << @parts[:date_fact] = DateFact.new(column, schema)
502
+ if column['schemaReference'] then
503
+ @parts[:date_ref] = DateReference.new column, schema
504
+ else
505
+ @attributes << @parts[:date_attr] = DateAttribute.new(column, schema)
506
+ end
507
+ if column['datetime'] then
508
+ puts "*** datetime"
509
+ @facts << @parts[:time_fact] = TimeFact.new(column, schema)
510
+ if column['schemaReference'] then
511
+ @parts[:time_ref] = TimeReference.new column, schema
512
+ else
513
+ @attributes << @parts[:time_attr] = TimeAttribute.new(column, schema)
514
+ end
515
+ end
516
+ end
517
+
518
+ def to_maql_create
519
+ @parts.values.map { |v| v.to_maql_create }.join "\n"
520
+ end
521
+
522
+ def to_maql_drop
523
+ @parts.values.map { |v| v.to_maql_drop }.join "\n"
524
+ end
525
+ end
526
+
527
+ ##
528
+ # Base class for GoodData attribute and fact folder abstractions
529
+ #
530
+ class Folder < MdObject
531
+ def initialize(title)
532
+ @title = title
533
+ @name = title
534
+ end
535
+
536
+ def to_maql_create
537
+ "CREATE FOLDER {#{type_prefix}.#{Model::to_id(name)}}" \
538
+ + " VISUAL (#{visual}) TYPE #{type};\n"
539
+ end
540
+ end
541
+
542
+ ##
543
+ # GoodData attribute folder abstraction
544
+ #
545
+ class AttributeFolder < Folder
546
+ def type; "ATTRIBUTE"; end
547
+ def type_prefix; "dim"; end
548
+ end
549
+
550
+ ##
551
+ # GoodData fact folder abstraction
552
+ #
553
+ class FactFolder < Folder
554
+ def type; "FACT"; end
555
+ def type_prefix; "ffld"; end
556
+ end
557
+ end
558
+ end