smartfm 0.3.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.
Files changed (40) hide show
  1. data/ChangeLog +38 -0
  2. data/README +45 -0
  3. data/Rakefile +155 -0
  4. data/examples/pure_ruby.rb +118 -0
  5. data/lib/ext/hash.rb +52 -0
  6. data/lib/smartfm.rb +16 -0
  7. data/lib/smartfm/core.rb +3 -0
  8. data/lib/smartfm/core/auth.rb +39 -0
  9. data/lib/smartfm/core/config.rb +51 -0
  10. data/lib/smartfm/core/version.rb +14 -0
  11. data/lib/smartfm/model.rb +5 -0
  12. data/lib/smartfm/model/base.rb +26 -0
  13. data/lib/smartfm/model/item.rb +174 -0
  14. data/lib/smartfm/model/list.rb +138 -0
  15. data/lib/smartfm/model/sentence.rb +118 -0
  16. data/lib/smartfm/model/user.rb +108 -0
  17. data/lib/smartfm/rest_client.rb +8 -0
  18. data/lib/smartfm/rest_client/base.rb +173 -0
  19. data/lib/smartfm/rest_client/item.rb +14 -0
  20. data/lib/smartfm/rest_client/list.rb +15 -0
  21. data/lib/smartfm/rest_client/sentence.rb +12 -0
  22. data/lib/smartfm/rest_client/user.rb +13 -0
  23. data/spec/ext/hash_spec.rb +11 -0
  24. data/spec/smartfm/core/auth_spec.rb +39 -0
  25. data/spec/smartfm/core/config_spec.rb +34 -0
  26. data/spec/smartfm/core/version_spec.rb +19 -0
  27. data/spec/smartfm/model/base_spec.rb +40 -0
  28. data/spec/smartfm/model/item_spec.rb +41 -0
  29. data/spec/smartfm/model/list_spec.rb +7 -0
  30. data/spec/smartfm/model/sentence_spec.rb +7 -0
  31. data/spec/smartfm/model/user_spec.rb +90 -0
  32. data/spec/smartfm/rest_client/base_spec.rb +9 -0
  33. data/spec/smartfm/rest_client/item_spec.rb +7 -0
  34. data/spec/smartfm/rest_client/list_spec.rb +7 -0
  35. data/spec/smartfm/rest_client/sentence_spec.rb +7 -0
  36. data/spec/smartfm/rest_client/user_spec.rb +7 -0
  37. data/spec/spec_helper.rb +18 -0
  38. data/test/smartfm_test.rb +8 -0
  39. data/test/test_helper.rb +3 -0
  40. metadata +132 -0
@@ -0,0 +1,108 @@
1
+ class Smartfm::User < Smartfm::Base
2
+ ATTRIBUTES = [:username, :profile]
3
+ attr_reader *ATTRIBUTES
4
+
5
+ class Profile < Smartfm::Base
6
+ ATTRIBUTES = [:name, :gender, :birthday, :description, :blog_url, :profile_url, :foaf_url, :icon_url]
7
+ attr_reader *ATTRIBUTES
8
+
9
+ def initialize(params = {})
10
+ @name = params[:name]
11
+ @gender = params[:gender]
12
+ @birthday = (Date.parse(params[:birthday]) rescue nil)
13
+ @description = params[:description]
14
+ @blog_url = params[:blog_url]
15
+ @profile_url = params[:profile_url]
16
+ @foaf_url = params[:foaf_url]
17
+ @icon_url = params[:icon_url]
18
+ end
19
+ end
20
+
21
+ class Study < Smartfm::Base
22
+ ATTRIBUTES = [:today, :results, :total_summary]
23
+ attr_reader *ATTRIBUTES
24
+
25
+ class Result < Smartfm::Base
26
+ ATTRIBUTES = [:timestamp, :seconds, :totals, :seen, :completed, :date]
27
+ attr_reader *ATTRIBUTES
28
+
29
+ def initialize(params = {})
30
+ @timestamp = (params[:timestamp].to_i rescue nil)
31
+ @seconds = (params[:seconds].to_i rescue nil)
32
+ @totals = {
33
+ :seconds => (params[:totals][:seconds].to_i rescue nil),
34
+ :seen => (params[:totals][:seen].to_i rescue nil),
35
+ :completed => (params[:totals][:completed].to_i rescue nil)
36
+ }
37
+ @seen = (params[:seen].to_i rescue nil)
38
+ @completed = (params[:completed].to_i rescue nil)
39
+ @date = (Date.parse(params[:date]) rescue nil)
40
+ end
41
+ end
42
+
43
+ class TotalSummary < Smartfm::Base
44
+ ATTRIBUTES = [:studied, :completed, :performance, :best_speed, :best_score]
45
+ attr_reader *ATTRIBUTES
46
+
47
+ def initialize(params = {})
48
+ @studied = params[:studied]
49
+ @completed = params[:completed]
50
+ @performance = params[:performance]
51
+ @best_speed = params[:best_speed]
52
+ @best_score = params[:best_score]
53
+ end
54
+ end
55
+
56
+ def initialize(params = {})
57
+ @today = (Date.parse(params[:today]) rescue nil)
58
+ @results = self.deserialize(params[:study_results], :as => Smartfm::User::Study::Result)
59
+ @total_summary = self.deserialize(params[:total_summary], :as => Smartfm::User::Study::TotalSummary)
60
+ end
61
+
62
+ end
63
+
64
+ def self.find(username, params = {})
65
+ params[:username] = username
66
+ hash = Smartfm::RestClient::User.find(params)
67
+ self.deserialize(hash)
68
+ end
69
+
70
+ def self.matching(keyword, params = {})
71
+ params[:keyword] = keyword
72
+ hash = Smartfm::RestClient::User.matching(params)
73
+ self.deserialize(hash) || []
74
+ end
75
+
76
+ def initialize(params)
77
+ @profile = Profile.new(params[:profile])
78
+ @username = params[:username]
79
+ end
80
+
81
+ def items(params = {})
82
+ hash = Smartfm::RestClient::User.items(params.merge(:username => self.username))
83
+ self.deserialize(hash, :as => Smartfm::Item) || []
84
+ end
85
+
86
+ def lists(params = {})
87
+ hash = Smartfm::RestClient::User.lists(params.merge(:username => self.username))
88
+ self.deserialize(hash, :as => Smartfm::List) || []
89
+ end
90
+
91
+ def friends(params = {})
92
+ hash = Smartfm::RestClient::User.friends(params.merge(:username => self.username))
93
+ self.deserialize(hash) || []
94
+ end
95
+
96
+ def followers(params = {})
97
+ hash = Smartfm::RestClient::User.followers(params.merge(:username => self.username))
98
+ self.deserialize(hash) || []
99
+ end
100
+
101
+ def study(params = {})
102
+ params[:application] ||= 'iknow'
103
+ return nil unless ['iknow', 'dictation', 'brainspeed', ].include?(params[:application])
104
+ hash = Smartfm::RestClient::User.study_results(params.merge(:username => self.username))
105
+ self.deserialize(hash, :as => Smartfm::User::Study)
106
+ end
107
+
108
+ end
@@ -0,0 +1,8 @@
1
+ module Smartfm::RestClient
2
+ end
3
+
4
+ require 'smartfm/rest_client/base'
5
+ require 'smartfm/rest_client/user'
6
+ require 'smartfm/rest_client/list'
7
+ require 'smartfm/rest_client/item'
8
+ require 'smartfm/rest_client/sentence'
@@ -0,0 +1,173 @@
1
+ require 'timeout'
2
+
3
+ class Smartfm::RestClient::Base
4
+
5
+ class RESTError < Exception
6
+ attr_accessor :code, :message
7
+
8
+ def initialize(params = {})
9
+ self.code = params[:code]
10
+ self.message = params[:message]
11
+ end
12
+
13
+ def to_s
14
+ "HTTP #{@code}: #{@message}"
15
+ end
16
+ end
17
+
18
+ def self.valid_action?(action) ; self::ACTIONS.keys.include? action.to_sym end
19
+ def self.path(action) ; self::ACTIONS[action.to_sym][:path] end
20
+ def self.http_method(action) ; self::ACTIONS[action.to_sym][:http_method] || :get end
21
+
22
+ def self.method_missing(action, *args)
23
+ # GET methods are handled here
24
+ # POST and DELETE methods should be implemented in each class
25
+ super unless self.valid_action?(action)
26
+ case self.http_method(action)
27
+ when :get
28
+ path, params = path_with_params(self.path(action), args[0])
29
+ http_get(path, params)
30
+ when :post
31
+ path, params = path_with_params(self.path(action), args[1])
32
+ http_post(auth(args[0]), path, params)
33
+ when :delete
34
+ path, params = path_with_params(self.path(action), args[1])
35
+ http_delete(auth(args[0]), path, params)
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def self.config; Smartfm::Config end
42
+
43
+ def self.auth(auth)
44
+ if auth.is_a?(Smartfm::Auth)
45
+ auth
46
+ else
47
+ raise ArgumentError.new("Authorized Smartfm::Auth instance is required")
48
+ end
49
+ end
50
+
51
+ def self.api_key_required
52
+ raise ArgumentError.new("smart.fm API key is required") if self.config.api_key == ''
53
+ end
54
+
55
+ def self.raise_rest_error(response)
56
+ raise RESTError.new(:code => response.code, :message => response.message)
57
+ end
58
+
59
+ def self.handle_rest_response(response, format)
60
+ raise_rest_error(response) unless response.is_a?(Net::HTTPSuccess)
61
+ case format
62
+ when :json
63
+ handle_json_response(response.body)
64
+ when :nothing
65
+ # success => nothing / failure => json error
66
+ begin
67
+ handle_json_response(response.body)
68
+ rescue Exception => e
69
+ e.is_a?(RESTError) ? raise(e) : :success
70
+ end
71
+ else
72
+ begin
73
+ handle_json_response(response.body)
74
+ rescue Exception => e
75
+ e.is_a?(RESTError) ? raise(e) : response.body
76
+ end
77
+ end
78
+ end
79
+
80
+ def self.handle_json_response(json_response)
81
+ hash = JSON.parse(json_response)
82
+ unless (hash['error'].nil? rescue :success) # success response may be Array, not Hash.
83
+ if hash['error']['code'] == 404
84
+ return nil
85
+ else
86
+ raise RESTError.new(:code => hash['error']['code'], :message => hash['error']['message'])
87
+ end
88
+ end
89
+ hash
90
+ end
91
+
92
+ def self.http_header
93
+ @@http_header ||= {
94
+ 'User-Agent' => "#{self.config.application_name} v#{Smartfm::Version.to_version} [#{self.config.user_agent}]",
95
+ 'Accept' => 'text/x-json',
96
+ 'X-smartfm-Gem-Client' => self.config.application_name,
97
+ 'X-smartfm-Gem-Client-Version' => self.config.application_version,
98
+ 'X-smartfm-Gem-Client-URL' => self.config.application_url,
99
+ }
100
+ end
101
+
102
+ def self.http_connect
103
+ http = Net::HTTP.new(self.config.api_host, self.config.api_port)
104
+ http.start do |conn|
105
+ request, format = yield
106
+ begin
107
+ timeout(self.config.timeout) do
108
+ response = conn.request(request)
109
+ handle_rest_response(response, format)
110
+ end
111
+ rescue
112
+ raise RESTError.new(:code => 408, :message => "smart.fm Gem Timeout (#{self.config.timeout} [sec])")
113
+ end
114
+ end
115
+ end
116
+
117
+ def self.path_with_params(path, params = {})
118
+ path_with_params = path.clone
119
+ unless params.empty?
120
+ params.each do |key, value|
121
+ if path_with_params=~/__#{key}__/
122
+ path_with_params.sub!(/__#{key}__/, URI.encode(value.to_s))
123
+ params.delete(key)
124
+ end
125
+ end
126
+ end
127
+ return path_with_params, params
128
+ end
129
+
130
+ def self.http_get(path, params = {})
131
+ http_connect do
132
+ params.merge!(:api_key => self.config.api_key) unless self.config.api_key == ''
133
+ path = (params.size > 0) ? "#{path}?#{params.to_http_str}" : path
134
+ get_req = Net::HTTP::Get.new(path, http_header)
135
+ [get_req, :json]
136
+ end
137
+ end
138
+
139
+ def self.http_post(auth, path, params = {})
140
+ self.api_key_required
141
+ params.merge!(:api_key => self.config.api_key)
142
+ case auth.mode
143
+ when :oauth
144
+ response = auth.auth_token.post(path, params, http_header)
145
+ handle_rest_response(response, :text)
146
+ when :basic_auth
147
+ http_connect do
148
+ post_req = Net::HTTP::Post.new(path, http_header)
149
+ post_req.body = params.to_http_str
150
+ post_req.basic_auth(auth.account.username, auth.account.password)
151
+ [post_req, :text]
152
+ end
153
+ end
154
+ end
155
+
156
+ def self.http_delete(auth, path, params = {})
157
+ self.api_key_required
158
+ params.merge!(:api_key => self.config.api_key)
159
+ case auth.mode
160
+ when :oauth
161
+ response = auth.auth_token.delete(path, params.stringfy_keys!.stringfy_values!)
162
+ handle_rest_response(response, :nothing)
163
+ when :basic_auth
164
+ http_connect do
165
+ delete_req = Net::HTTP::Post.new(path, http_header)
166
+ delete_req.body = params.merge(:_method => 'DELETE').to_http_str
167
+ delete_req.basic_auth(auth.account.username, auth.account.password)
168
+ [delete_req, :nothing]
169
+ end
170
+ end
171
+ end
172
+
173
+ end
@@ -0,0 +1,14 @@
1
+ class Smartfm::RestClient::Item < Smartfm::RestClient::Base
2
+
3
+ ACTIONS = {
4
+ :recent => { :path => '/items' },
5
+ :find => { :path => '/items/__id__' },
6
+ :matching => { :path => '/items/matching/__keyword__' },
7
+ :extract => { :path => '/items/extract', },
8
+ :create => { :path => '/items', :http_method => :post },
9
+ :add_image => { :path => '/items/__id__/images', :http_method => :post },
10
+ :add_sound => { :path => '/items/__id__/sounds', :http_method => :post },
11
+ :add_tags => { :path => '/items/__id__/tags', :http_method => :post }
12
+ }
13
+
14
+ end
@@ -0,0 +1,15 @@
1
+ class Smartfm::RestClient::List < Smartfm::RestClient::Base
2
+
3
+ ACTIONS = {
4
+ :recent => { :path => '/lists' },
5
+ :find => { :path => '/lists/__id__' },
6
+ :items => { :path => '/lists/__id__/items' },
7
+ :sentences => { :path => '/lists/__id__/sentences' },
8
+ :matching => { :path => '/lists/matching/__keyword__' },
9
+ :create => { :path => '/lists', :http_method => :post },
10
+ :delete => { :path => '/lists/__id__', :http_method => :delete },
11
+ :add_item => { :path => '/lists/__list_id__/items', :http_method => :post },
12
+ :delete_item => { :path => '/lists/__list_id__/items/__id__', :http_method => :delete }
13
+ }
14
+
15
+ end
@@ -0,0 +1,12 @@
1
+ class Smartfm::RestClient::Sentence < Smartfm::RestClient::Base
2
+
3
+ ACTIONS = {
4
+ :recent => { :path => '/sentences' },
5
+ :find => { :path => '/sentences/__id__' },
6
+ :matching => { :path => '/sentences/matching/__keyword__' },
7
+ :create => { :path => '/sentences', :http_method => :post },
8
+ :add_image => { :path => '/sentences/__id__/images', :http_method => :post },
9
+ :add_sound => { :path => '/sentences/__id__/sounds', :http_method => :post }
10
+ }
11
+
12
+ end
@@ -0,0 +1,13 @@
1
+ class Smartfm::RestClient::User < Smartfm::RestClient::Base
2
+
3
+ ACTIONS = {
4
+ :find => { :path => '/users/__username__' },
5
+ :lists => { :path => '/users/__username__/lists' },
6
+ :items => { :path => '/users/__username__/items' },
7
+ :friends => { :path => '/users/__username__/friends' },
8
+ :followers => { :path => '/users/__username__/followers' },
9
+ :study_results => { :path => '/users/__username__/study_results/__application__' },
10
+ :matching => { :path => '/users/matching/__keyword__' }
11
+ }
12
+
13
+ end
@@ -0,0 +1,11 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ describe Hash, "#to_http_str" do
4
+ before do
5
+ @params = {:page => 3, :per_page => 100}
6
+ end
7
+ it "should return http_parameter as String" do
8
+ @params.to_http_str.should be_a(String)
9
+ @params.to_http_str.should match(/^(page=3&per_page=100|per_page=100&page=3)$/)
10
+ end
11
+ end
@@ -0,0 +1,39 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
2
+
3
+ basic_auth_params = { :username => 'matake', :password => 'password' }
4
+ oauth_params = { :token => '1a2b3c4d5', :secret => 'z0y9x8w7v6' }
5
+
6
+ describe Smartfm::Auth, '.new' do
7
+ before do
8
+ @basic_auth = Smartfm::Auth.new(basic_auth_params)
9
+ @oauth = Smartfm::Auth.new(oauth_params)
10
+ end
11
+
12
+ it "should choose #{:basic_auth} mode" do
13
+ @basic_auth.mode.should equal(:basic_auth)
14
+ @basic_auth.should respond_to(:account)
15
+ @basic_auth.account.should respond_to(:username)
16
+ @basic_auth.account.should respond_to(:password)
17
+ @basic_auth.account.should_not respond_to(:token)
18
+ @basic_auth.account.should_not respond_to(:secret)
19
+ end
20
+
21
+ it "should choose #{:oauth} mode" do
22
+ @oauth.mode.should equal(:oauth)
23
+ @oauth.should respond_to(:auth_token)
24
+ @oauth.auth_token.should respond_to(:token)
25
+ @oauth.auth_token.should respond_to(:secret)
26
+ @oauth.auth_token.should_not respond_to(:username)
27
+ @oauth.auth_token.should_not respond_to(:password)
28
+ end
29
+ end
30
+
31
+ describe Smartfm::Auth, '.consumer' do
32
+ it "should be an instance of OAuth::Consumer" do
33
+ Smartfm::Auth.consumer.should be_a(OAuth::Consumer)
34
+ end
35
+
36
+ it "should be for http://api.smart.fm" do
37
+ Smartfm::Auth.consumer.site.should eql("http://api.smart.fm")
38
+ end
39
+ end
@@ -0,0 +1,34 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
2
+
3
+ describe Smartfm::Config, ".init" do
4
+ before do
5
+ Smartfm::Config.init
6
+ end
7
+
8
+ it "should return init config" do
9
+ Smartfm::Config.init.should equal(Smartfm::Config.instance)
10
+ end
11
+
12
+ it "should change config with block" do
13
+ Smartfm::Config.init do |conf|
14
+ Smartfm::Config::ATTRIBUTES.each do |method|
15
+ conf.send("#{method}=", "fuckin_windows!")
16
+ end
17
+ end
18
+ Smartfm::Config::ATTRIBUTES.each do |method|
19
+ Smartfm::Config.send("#{method}").should eql("fuckin_windows!")
20
+ end
21
+ end
22
+
23
+ after do
24
+ Smartfm::Config.init
25
+ end
26
+ end
27
+
28
+ (Smartfm::Config::ATTRIBUTES + [:base_url, :api_base_url]).each do |method|
29
+ describe Smartfm::Config, ".#{method}" do
30
+ it "should be same with Smartfm::Config.instance.#{method}" do
31
+ Smartfm::Config.send(method).should eql(Smartfm::Config.instance.send(method))
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,19 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
2
+
3
+ version_params = [Smartfm::Version::MAJOR, Smartfm::Version::MINOR, Smartfm::Version::REVISION]
4
+ expected = {
5
+ :version => version_params.join('.'),
6
+ :name => version_params.join('_')
7
+ }
8
+
9
+ describe Smartfm::Version, ".to_version" do
10
+ it "should return #{expected[:version]}" do
11
+ Smartfm::Version.to_version.should eql(expected[:version])
12
+ end
13
+ end
14
+
15
+ describe Smartfm::Version, ".to_name" do
16
+ it "should return #{expected[:name]}" do
17
+ Smartfm::Version.to_name.should eql(expected[:name])
18
+ end
19
+ end