gooddata 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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