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,42 @@
1
+ module GoodData
2
+ class Links
3
+ attr_reader :data
4
+
5
+ def initialize(items)
6
+ @data = {}
7
+ items.values[0]['links'].each do |item|
8
+ category = item['category']
9
+ if @data[category] then
10
+ if @data[category]['category'] == category then
11
+ @data[category] = { @data[category]['identifier'] => @data[category] }
12
+ end
13
+ @data[category][item['identifier']] = item
14
+ else
15
+ @data[category] = item
16
+ end
17
+ end
18
+ end
19
+
20
+ def links(category, identifier = nil)
21
+ return Links.new(GoodData.get(self[category])) unless identifier
22
+ Links.new GoodData.get(get(category, identifier))
23
+ end
24
+
25
+ def [](category)
26
+ return @data[category]['link'] if @data[category] && @data[category]['link']
27
+ @data[category]
28
+ end
29
+
30
+ def is_unique?(category)
31
+ @data[category]['link'].is_a? String
32
+ end
33
+
34
+ def is_ambiguous?(category)
35
+ !is_unique?(category)
36
+ end
37
+
38
+ def get(category, identifier)
39
+ self[category][identifier]
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,82 @@
1
+ require 'gooddata/model'
2
+
3
+ module GoodData
4
+ class MdObject
5
+ MD_OBJ_CTG = 'obj'
6
+ IDENTIFIERS_CFG = 'instance-identifiers'
7
+
8
+ class << self
9
+ def [](id)
10
+ raise "Cannot search for nil #{self.class}" unless id
11
+ if id.is_a? Integer or id =~ /^\d+$/
12
+ uri = "#{GoodData.project.md.link(MD_OBJ_CTG)}/#{id}"
13
+ elsif id !~ /\//
14
+ uri = identifier_to_uri id
15
+ elsif id =~ /^\//
16
+ uri = id
17
+ else
18
+ raise "Unexpected object id format: expected numeric ID, identifier with no slashes or an URI starting with a slash"
19
+ end
20
+ self.new((GoodData.get uri).values[0])
21
+ end
22
+
23
+ private
24
+
25
+ def identifier_to_uri(id)
26
+ raise NoProjectError.new "Connect to a project before searching for an object" unless GoodData.project
27
+ uri = GoodData.project.md[IDENTIFIERS_CFG]
28
+ response = GoodData.post uri, { 'identifierToUri' => [id ] }
29
+ response['identifiers'][0]['uri']
30
+ end
31
+ end
32
+
33
+ def initialize(json)
34
+ @json = json
35
+ end
36
+
37
+ def delete
38
+ raise "Project '#{title}' with id #{uri} is already deleted" if state == :deleted
39
+ GoodData.delete @json['links']['self']
40
+ end
41
+
42
+ def uri
43
+ meta['uri']
44
+ end
45
+
46
+ def identifier
47
+ meta['identifier']
48
+ end
49
+
50
+ def title
51
+ meta['title']
52
+ end
53
+
54
+ def meta
55
+ @json['meta']
56
+ end
57
+
58
+ def content
59
+ @json['content']
60
+ end
61
+
62
+ def project
63
+ @project ||= Project[uri.gsub(/\/obj\/\d+$/, '')]
64
+ end
65
+ end
66
+
67
+ class DataSet < MdObject
68
+ SLI_CTG = 'singleloadinterface'
69
+ DS_SLI_CTG = 'dataset-singleloadinterface'
70
+
71
+ def sli_enabled?
72
+ content['mode'] == 'SLI'
73
+ end
74
+
75
+ def sli
76
+ raise NoProjectError.new "Connect to a project before searching for an object" unless GoodData.project
77
+ slis = GoodData.project.md.links(Model::LDM_CTG).links(SLI_CTG)[DS_SLI_CTG]
78
+ uri = slis[identifier]['link']
79
+ MdObject[uri]
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,32 @@
1
+ module GoodData
2
+ class Profile
3
+ private_class_method :new
4
+ attr_reader :user
5
+
6
+ class << self
7
+ def load
8
+ # GoodData.logger.info "Loading user profile..."
9
+ Profile.send 'new'
10
+ end
11
+ end
12
+
13
+ def projects
14
+ @json['accountSetting']['links']['projects']
15
+ end
16
+
17
+ def to_json
18
+ @json
19
+ end
20
+
21
+ def [](key)
22
+ @json['accountSetting'][key]
23
+ end
24
+
25
+ private
26
+
27
+ def initialize
28
+ @json = GoodData.get GoodData.connection.user['profile']
29
+ @user = @json['accountSetting']['firstName'] + " " + @json['accountSetting']['lastName']
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,136 @@
1
+ require 'zip/zip'
2
+ require 'fileutils'
3
+
4
+ module GoodData
5
+ class NoProjectError < RuntimeError ; end
6
+
7
+ class Project
8
+ USERSPROJECTS_PATH = '/gdc/account/profile/%s/projects'
9
+ PROJECTS_PATH = '/gdc/projects'
10
+ PROJECT_PATH = '/gdc/projects/%s'
11
+ SLIS_PATH = '/ldm/singleloadinterface'
12
+
13
+ attr_accessor :connection
14
+
15
+ class << self
16
+ # Returns an array of all projects accessible by
17
+ # current user
18
+ def all
19
+ json = GoodData.get GoodData.profile.projects
20
+ json['projects'].map do |project|
21
+ Project.new project['project']
22
+ end
23
+ end
24
+
25
+ # Returns a Project object identified by given string
26
+ # The following identifiers are accepted
27
+ # - /gdc/md/<id>
28
+ # - /gdc/projects/<id>
29
+ # - <id>
30
+ #
31
+ def [](id)
32
+ if id.to_s !~ /^(\/gdc\/(projects|md)\/)?[a-zA-Z\d]+$/
33
+ raise ArgumentError.new("wrong type of argument. Should be either project ID or path")
34
+ end
35
+
36
+ id = id.match(/[a-zA-Z\d]+$/)[0] if id =~ /\//
37
+
38
+ response = GoodData.get PROJECT_PATH % id
39
+ Project.new response['project']
40
+ end
41
+
42
+ # Create a project from a given attributes
43
+ # Expected keys:
44
+ # - :title (mandatory)
45
+ # - :summary
46
+ # - :template (default /projects/blank)
47
+ #
48
+ def create(attributes)
49
+ GoodData.logger.info "Creating project #{attributes[:title]}"
50
+
51
+ json = {
52
+ 'meta' => {
53
+ 'title' => attributes[:title],
54
+ 'summary' => attributes[:summary]
55
+ },
56
+ 'content' => {
57
+ # 'state' => 'ENABLED',
58
+ 'guidedNavigation' => 1
59
+ }
60
+ }
61
+
62
+ json['meta']['projectTemplate'] = attributes[:template] if attributes[:template] && !attributes[:template].empty?
63
+ project = Project.new json
64
+ project.save
65
+ project
66
+ end
67
+ end
68
+
69
+ def initialize(json)
70
+ @json = json
71
+ end
72
+
73
+ def save
74
+ response = GoodData.post PROJECTS_PATH, { 'project' => @json }
75
+ if uri == nil
76
+ response = GoodData.get response['uri']
77
+ @json = response['project']
78
+ end
79
+ end
80
+
81
+ def delete
82
+ raise "Project '#{title}' with id #{uri} is already deleted" if state == :deleted
83
+ GoodData.delete @json['links']['self']
84
+ end
85
+
86
+ def uri
87
+ @json['links']['self'] if @json['links'] && @json['links']['self']
88
+ end
89
+
90
+ def title
91
+ @json['meta']['title'] if @json['meta']
92
+ end
93
+
94
+ def state
95
+ @json['content']['state'].downcase.to_sym if @json['content'] && @json['content']['state']
96
+ end
97
+
98
+ def md
99
+ @md ||= Links.new GoodData.get(@json['links']['metadata'])
100
+ end
101
+
102
+ # Creates a data set within the project
103
+ #
104
+ # == Usage
105
+ # p.add_dataset 'Test', [ { 'name' => 'a1', 'type' => 'ATTRIBUTE' ... } ... ]
106
+ # p.add_dataset 'title' => 'Test', 'columns' => [ { 'name' => 'a1', 'type' => 'ATTRIBUTE' ... } ... ]
107
+ #
108
+ def add_dataset(schema, columns = nil)
109
+ schema = { 'title' => schema, 'columns' => columns } if columns
110
+ schema = Model::Schema.new schema if schema.is_a? Hash
111
+ raise ArgumentError.new("Required either schema object or title plus columns array") unless schema.is_a? Model::Schema
112
+ Model.add_schema schema, self
113
+ end
114
+
115
+ def upload(file, schema)
116
+ schema.upload file, self
117
+ end
118
+
119
+ def slis
120
+ link = "#{@json['links']['metadata']}#{SLIS_PATH}"
121
+ Metadata.new GoodData.get(link)
122
+ end
123
+
124
+ def datasets
125
+ datasets_uri = "#{md['data']}/sets"
126
+ response = GoodData.get datasets_uri
127
+ response['dataSetsInfo']['sets'].map do |ds|
128
+ DataSet.new ds
129
+ end
130
+ end
131
+
132
+ def to_json
133
+ @json
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,3 @@
1
+ module GoodData
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'gooddata'
8
+
9
+ class Test::Unit::TestCase
10
+ end
@@ -0,0 +1,85 @@
1
+ require 'logger'
2
+ require 'tempfile'
3
+
4
+ require 'helper'
5
+ require 'gooddata/command'
6
+
7
+ GoodData.logger = Logger.new(STDOUT)
8
+
9
+ class TestRestApiBasic < Test::Unit::TestCase
10
+ context "datasets command" do
11
+ SAMPLE_DATASET_CONFIG = {
12
+ "columns" => [
13
+ {
14
+ "type" => "CONNECTION_POINT",
15
+ "name" => "A1",
16
+ "title" =>"A1"
17
+ },
18
+ {
19
+ "type" => "ATTRIBUTE",
20
+ "name" => "A2",
21
+ "title" => "A2",
22
+ "folder"=> "Test"
23
+ },
24
+ {
25
+ "type" => "FACT",
26
+ "name" => "F2",
27
+ "title" => "F2 \"asdasd\"",
28
+ "folder"=> "Test"
29
+ }
30
+ ],
31
+ "title" => "Test"
32
+ }
33
+
34
+ should "list datasets" do
35
+ GoodData::Command.run "datasets", [ "--project", "FoodMartDemo" ]
36
+ end
37
+
38
+ should "apply a dataset model" do
39
+ GoodData::Command.connect
40
+ project = GoodData::Project.create \
41
+ :title => "gooddata-ruby TestRestApi #{Time.new.to_i}", :template => '/projectTemplates/empty/1'
42
+
43
+ Tempfile.open 'gdrb-test-' do |file|
44
+ file.puts SAMPLE_DATASET_CONFIG.to_json
45
+ file.close
46
+ GoodData::Command.run "datasets:apply", [ "--project", project.uri, file.path ]
47
+ end
48
+ project.delete
49
+ end
50
+ end
51
+
52
+ context "projects command" do
53
+ should "list projects" do
54
+ GoodData::Command.run "projects", []
55
+ end
56
+ end
57
+
58
+ context "api command" do
59
+ should "perform a test login" do
60
+ GoodData::Command.run "api:test", []
61
+ end
62
+
63
+ should "get FoodMartDemo metadata" do
64
+ GoodData::Command.run "api:get", [ '/gdc/md/FoodMartDemo' ]
65
+ end
66
+ end
67
+
68
+ context "profile command" do
69
+ should "show my GoodData profile" do
70
+ GoodData::Command.run "profile", []
71
+ end
72
+ end
73
+
74
+ context "help command" do
75
+ should "print help screen" do
76
+ GoodData::Command.run "help", []
77
+ end
78
+ end
79
+
80
+ context "version command" do
81
+ should "print version" do
82
+ GoodData::Command.run "version", []
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,46 @@
1
+ require 'helper'
2
+ require 'gooddata/command'
3
+
4
+ class TestGuesser < Test::Unit::TestCase
5
+ should "order LDM types as follows: cp, fact, date, attribute" do
6
+ assert_equal [ :connection_point, :fact, :date, :attribute ], \
7
+ GoodData::Command::Guesser::sort_types([ :fact, :attribute, :connection_point, :date ])
8
+ assert_equal [ :fact ], GoodData::Command::Guesser::sort_types([ :fact ])
9
+ assert_equal [], GoodData::Command::Guesser::sort_types([])
10
+ end
11
+
12
+ should "guess facts, dates and connection points from a simple CSV" do
13
+ csv = [
14
+ [ 'cp', 'a1', 'a2', 'd1', 'd2', 'f'],
15
+ [ '1', 'one', 'huh', '2001-01-02', nil, '-1' ],
16
+ [ '2', 'two', 'blah', nil, '1970-10-23', '2.3' ],
17
+ [ '3', 'three', 'bleh', '0000-00-00', nil, '-3.14159'],
18
+ [ '4', 'one', 'huh', '2010-02-28 08:12:34', '1970-10-23', nil ]
19
+ ]
20
+ fields = GoodData::Command::Guesser.new(csv).guess(csv.size + 10)
21
+
22
+ assert_kind_of Hash, fields, "guesser should return a Hash"
23
+ fields.each do |field, info|
24
+ assert_kind_of Array, info, "guess for '%s' is not an Array" % field
25
+ end
26
+
27
+ type_msg_fmt = 'checking guessed types of "%s"'
28
+
29
+ assert_equal GoodData::Command::Guesser::sort_types([
30
+ :connection_point, :fact, :attribute
31
+ ]), fields['cp'], type_msg_fmt % 'cp'
32
+
33
+ assert_equal [ :attribute ], fields['a1'], type_msg_fmt % 'a1'
34
+ assert_equal [ :attribute ], fields['a2'], type_msg_fmt % 'a2'
35
+
36
+ assert_equal GoodData::Command::Guesser::sort_types([
37
+ :attribute, :connection_point, :date
38
+ ]), fields['d1'], type_msg_fmt % 'd1'
39
+ assert_equal GoodData::Command::Guesser::sort_types([
40
+ :attribute, :date
41
+ ]), fields['d2'], type_msg_fmt % 'd2'
42
+ assert_equal GoodData::Command::Guesser::sort_types([
43
+ :attribute, :connection_point, :fact
44
+ ]), fields['f'], type_msg_fmt % 'f'
45
+ end
46
+ end
@@ -0,0 +1,63 @@
1
+ require 'logger'
2
+
3
+ require 'helper'
4
+ require 'gooddata/model'
5
+ require 'gooddata/command'
6
+
7
+ GoodData.logger = Logger.new(STDOUT)
8
+
9
+ class TestModel < Test::Unit::TestCase
10
+ COLUMNS = [
11
+ { 'type' => 'CONNECTION_POINT', 'name' => 'cp', 'title' => 'CP', 'folder' => 'test' },
12
+ { 'type' => 'ATTRIBUTE', 'name' => 'a1', 'title' => 'A1', 'folder' => 'test' },
13
+ { 'type' => 'ATTRIBUTE', 'name' => 'a2', 'title' => 'A2', 'folder' => 'test' },
14
+ { 'type' => 'DATE', 'name' => 'event', 'title' => 'Event', 'folder' => 'test' },
15
+ { 'type' => 'FACT', 'name' => 'f1', 'title' => 'F1', 'folder' => 'test' },
16
+ { 'type' => 'FACT', 'name' => 'f2', 'title' => 'F2', 'folder' => 'test' },
17
+ ]
18
+ SCHEMA = GoodData::Model::Schema.new 'title' => 'test', 'columns' => COLUMNS
19
+
20
+ context "GoodData model tools" do
21
+ # Initialize a GoodData connection using the credential
22
+ # stored in ~/.gooddata
23
+ setup do
24
+ GoodData::Command::connect
25
+ end
26
+
27
+ should "generate identifiers star ting with letters and without ugly characters" do
28
+ assert_equal 'fact.test.blah', GoodData::Model::Fact.new({ 'name' => 'blah' }, SCHEMA).identifier
29
+ assert_equal 'attr.test.blah', GoodData::Model::Attribute.new({ 'name' => '1_2_3 blah' }, SCHEMA).identifier
30
+ assert_equal 'dim.blaz', GoodData::Model::AttributeFolder.new(' b*ĺ*á#ž$').identifier
31
+ end
32
+
33
+ should "create a simple model in a sandbox project using Model.add_dataset" do
34
+ project = GoodData::Project.create :title => "gooddata-ruby test #{Time.new.to_i}"
35
+ GoodData.use project
36
+ objects = GoodData::Model.add_dataset 'Mrkev', COLUMNS
37
+
38
+ uris = objects['uris']
39
+ assert_equal "#{project.md['obj']}/1", uris[0]
40
+ # fetch last object (temporary objects can be placed at the begining of the list)
41
+ GoodData.get uris[uris.length - 1]
42
+
43
+ # created model should define SLI interface on the 'Mrkev' data set
44
+ # TODO move this into a standalone test covering gooddata/metadata.rb
45
+ ds = GoodData::DataSet['dataset.mrkev']
46
+ assert_not_nil ds
47
+
48
+ # clean-up
49
+ project.delete
50
+ end
51
+
52
+ should "create a simple model in a sandbox project using project.model.add_dataset" do
53
+ project = GoodData::Project.create :title => "gooddata-ruby test #{Time.new.to_i}"
54
+ objects = project.add_dataset 'Mrkev', COLUMNS
55
+
56
+ uris = objects['uris']
57
+ assert_equal "#{project.md['obj']}/1", uris[0]
58
+ # fetch last object (temporary objects can be placed at the begining of the list)
59
+ GoodData.get uris[uris.length - 1]
60
+ project.delete
61
+ end
62
+ end
63
+ end