gooddata 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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