shortwave 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +10 -0
- data/.gitmodules +3 -0
- data/LICENSE +20 -0
- data/README.rdoc +74 -0
- data/Rakefile +133 -0
- data/VERSION +1 -0
- data/lib/shortwave.rb +12 -0
- data/lib/shortwave/authentication.rb +107 -0
- data/lib/shortwave/facade.rb +3 -0
- data/lib/shortwave/facade/build/facade_builder.rb +199 -0
- data/lib/shortwave/facade/build/facade_template.erb +17 -0
- data/lib/shortwave/facade/lastfm.rb +878 -0
- data/lib/shortwave/facade/remote.rb +86 -0
- data/lib/shortwave/model/album.rb +33 -0
- data/lib/shortwave/model/artist.rb +62 -0
- data/lib/shortwave/model/base_model.rb +112 -0
- data/lib/shortwave/model/chart_dates.rb +15 -0
- data/lib/shortwave/model/comparison.rb +15 -0
- data/lib/shortwave/model/event.rb +55 -0
- data/lib/shortwave/model/group.rb +26 -0
- data/lib/shortwave/model/location.rb +45 -0
- data/lib/shortwave/model/playlist.rb +36 -0
- data/lib/shortwave/model/shout.rb +21 -0
- data/lib/shortwave/model/tag.rb +47 -0
- data/lib/shortwave/model/track.rb +71 -0
- data/lib/shortwave/model/user.rb +70 -0
- data/lib/shortwave/model/venue.rb +37 -0
- data/lib/shortwave/model/weekly_charts.rb +31 -0
- data/lib/shortwave/providers.rb +161 -0
- data/shortwave.gemspec +178 -0
- data/test/authentication_test.rb +64 -0
- data/test/build/build_test.rb +25 -0
- data/test/build/data/intro.yml +2 -0
- data/test/build/data/screens/album_addTags.html +1238 -0
- data/test/build/data/screens/intro_truncated.html +426 -0
- data/test/build/data/screens/tasteometer_compare.html +1274 -0
- data/test/build/data/screens/user_getLovedTracks.html +1278 -0
- data/test/build/data/screens/venue_search.html +1261 -0
- data/test/build/facade_builder_test.rb +23 -0
- data/test/build/parameter_test.rb +43 -0
- data/test/build/remote_method_test.rb +47 -0
- data/test/build/ruby_class_test.rb +12 -0
- data/test/build/ruby_method_test.rb +137 -0
- data/test/helper.rb +35 -0
- data/test/model/album_test.rb +62 -0
- data/test/model/artist_test.rb +103 -0
- data/test/model/chart_dates_test.rb +11 -0
- data/test/model/comparison_test.rb +18 -0
- data/test/model/data/album_info.xml +38 -0
- data/test/model/data/album_search.xml +210 -0
- data/test/model/data/artist_info.xml +58 -0
- data/test/model/data/artist_search.xml +109 -0
- data/test/model/data/artist_shouts.xml +546 -0
- data/test/model/data/artist_top_fans.xml +405 -0
- data/test/model/data/event_info.xml +47 -0
- data/test/model/data/group_members.xml +242 -0
- data/test/model/data/group_weekly_album_chart.xml +1754 -0
- data/test/model/data/group_weekly_artist_chart.xml +604 -0
- data/test/model/data/group_weekly_track_chart.xml +1005 -0
- data/test/model/data/location_events.xml +383 -0
- data/test/model/data/ok.xml +2 -0
- data/test/model/data/playlist_fetch.xml +227 -0
- data/test/model/data/tag_search.xml +110 -0
- data/test/model/data/tag_similar.xml +254 -0
- data/test/model/data/tag_top_albums.xml +805 -0
- data/test/model/data/tag_top_artists.xml +605 -0
- data/test/model/data/tag_top_tags.xml +1254 -0
- data/test/model/data/tag_top_tracks.xml +843 -0
- data/test/model/data/tag_weekly_chart_list.xml +57 -0
- data/test/model/data/tasteometer_compare.xml +54 -0
- data/test/model/data/track_info.xml +53 -0
- data/test/model/data/track_search.xml +195 -0
- data/test/model/data/user_chartlist.xml +90 -0
- data/test/model/data/user_info.xml +16 -0
- data/test/model/data/user_neighbours.xml +484 -0
- data/test/model/data/user_playlists.xml +17 -0
- data/test/model/data/user_recent_tracks.xml +124 -0
- data/test/model/data/user_recommended_artists.xml +454 -0
- data/test/model/data/user_shouts.xml +9 -0
- data/test/model/data/user_weekly_artist_chart.xml +478 -0
- data/test/model/data/venue_events.xml +556 -0
- data/test/model/data/venue_past_events.xml +1778 -0
- data/test/model/data/venue_search.xml +355 -0
- data/test/model/event_test.rb +63 -0
- data/test/model/group_test.rb +45 -0
- data/test/model/location_test.rb +25 -0
- data/test/model/playlist_test.rb +51 -0
- data/test/model/shout_test.rb +23 -0
- data/test/model/tag_test.rb +39 -0
- data/test/model/track_test.rb +67 -0
- data/test/model/user_test.rb +125 -0
- data/test/model/venue_test.rb +60 -0
- data/test/provider/album_provider_test.rb +26 -0
- data/test/provider/artist_provider_test.rb +25 -0
- data/test/provider/group_provider_test.rb +9 -0
- data/test/provider/location_provider_test.rb +9 -0
- data/test/provider/playlist_provider_test.rb +12 -0
- data/test/provider/tag_provider_test.rb +24 -0
- data/test/provider/track_provider_test.rb +26 -0
- data/test/provider/user_provider_test.rb +11 -0
- data/test/provider/venue_provider_test.rb +15 -0
- data/test/provider_test_helper.rb +27 -0
- data/test/remote_test.rb +26 -0
- metadata +209 -0
data/.document
ADDED
data/.gitignore
ADDED
data/.gitmodules
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Roland Swingler
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
= Shortwave
|
2
|
+
|
3
|
+
Shortwave aims to be a complete API wrapper for the Last.fm web service.
|
4
|
+
|
5
|
+
Shortwave is split into two: the Facade, which provides a low-level thin wrapper around the
|
6
|
+
remote api, and the Model (which uses the Facade) which provides a more OO view of the api.
|
7
|
+
It would be possible to build your own api on top of the Facade if you don't like the Model
|
8
|
+
api provided - the two are fairly separate. See the docs for the Facade model if you want to
|
9
|
+
use it directly.
|
10
|
+
|
11
|
+
<b>Shortwave is currently pre-alpha. This means both the API and dependencies may change quite
|
12
|
+
a bit over forthcoming releases.</b>
|
13
|
+
|
14
|
+
== Advantages over other libraries.
|
15
|
+
|
16
|
+
* Shortwave aims to be complete. The facade is autogenerated from the Last.fm api documentation
|
17
|
+
so as the last.fm api evolves it should be possible to keep it in sync relatively easily
|
18
|
+
* No enforced opinions - the Facade is a very thin layer, so you can use it directly and treat
|
19
|
+
the returned XML as you please.
|
20
|
+
* Supports all 3 authentication types Last.fm provide.
|
21
|
+
* Doesn't rely on static/class properties for things like session keys. This means you can build
|
22
|
+
applications which have multiple users signed in.
|
23
|
+
|
24
|
+
== Examples
|
25
|
+
|
26
|
+
You need to create a session using one of the Authentication mechanisms:
|
27
|
+
|
28
|
+
session = Shortwave::Authentication::Mobile.new("your_api_key", "your_app_secret")
|
29
|
+
session.authenticate("user","password")
|
30
|
+
|
31
|
+
See the documentation for each of the Session types for how to authenticate for your
|
32
|
+
platform. Once you have a session, you can get model objects from it:
|
33
|
+
|
34
|
+
session.artist.search("The feelies") # => returns a list of Artists
|
35
|
+
session.user.logged_in_user # => the current user
|
36
|
+
session.tag.popular # => the most popular tags
|
37
|
+
|
38
|
+
See the Provider docs for more details of how you can access models. Models have attributes
|
39
|
+
and links to other models.
|
40
|
+
|
41
|
+
venue = session.venue.search("Koko").first
|
42
|
+
venue.city # => "London"
|
43
|
+
venue.events # => "list of events"
|
44
|
+
venue.events.first.artist # => an Artist
|
45
|
+
|
46
|
+
== Current limiations & TODOs
|
47
|
+
|
48
|
+
* Paged methods only provide the first page of results when accessed through the model. The goal
|
49
|
+
is to make it transparent that you may have to make n remote calls to get a certain number of
|
50
|
+
results. You can, of course, access arbitrary numbers of results through the Facade methods.
|
51
|
+
* Not all attributes will be populated on Models when accessed from different method calls. This
|
52
|
+
is down to the set of data Last.fm returns. The goal is to have missing attributes be populated
|
53
|
+
as needed by further api calls.
|
54
|
+
* Might be nice to have Playlists use another ruby XSPF library for more functionality.
|
55
|
+
* Radio tuning not done yet.
|
56
|
+
* HTTP caching
|
57
|
+
* in-built request throttling
|
58
|
+
|
59
|
+
== Contributions
|
60
|
+
|
61
|
+
Contributions are welcome. Source is on github at http://github.com/knaveofdiamonds/shortwave
|
62
|
+
|
63
|
+
== Dependencies & Credits
|
64
|
+
|
65
|
+
Shortwave depends on the following gems:
|
66
|
+
|
67
|
+
* Nokogiri
|
68
|
+
* Restclient
|
69
|
+
|
70
|
+
Shortwave also includes a version of HappyMapper (under vendor) copyright John Nunemaker, hacked about to work directly with Nokogiri documents and fix some bugs. See vendor/happymapper/License for details.
|
71
|
+
|
72
|
+
== Copyright
|
73
|
+
|
74
|
+
Copyright (c) 2009 Roland Swingler. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require "lib/shortwave/facade/build/facade_builder"
|
6
|
+
|
7
|
+
include Shortwave::Facade::Build
|
8
|
+
namespace :facade do
|
9
|
+
directory "tmp/lastfm"
|
10
|
+
|
11
|
+
task :scrape_method_index => "tmp/lastfm" do
|
12
|
+
REMOTE_METHOD_DEFINITIONS = FacadeBuilder.new.remote_method_definitions("tmp/lastfm/intro.yml")
|
13
|
+
end
|
14
|
+
|
15
|
+
task :scrape => :scrape_method_index do
|
16
|
+
methods = REMOTE_METHOD_DEFINITIONS.values.inject {|a, b| a.merge(b) }
|
17
|
+
methods.each do |name, uri|
|
18
|
+
if ! File.exists?("tmp/lastfm/#{name}.html")
|
19
|
+
response = DocumentationRemote.get(uri)
|
20
|
+
File.open("tmp/lastfm/#{name}.html", "w") {|fh| fh.write(response) }
|
21
|
+
warn "Got HTML documentation for #{name}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
task :parse => :scrape do
|
27
|
+
REMOTE_METHODS = {}
|
28
|
+
REMOTE_METHOD_DEFINITIONS.each do |klass_name, methods|
|
29
|
+
REMOTE_METHODS[klass_name] = []
|
30
|
+
methods.keys.each do |method_name|
|
31
|
+
warn "parsing #{method_name}"
|
32
|
+
REMOTE_METHODS[klass_name] << RemoteMethod.new( File.read("tmp/lastfm/#{method_name}.html"))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Fixes any ommissions or mistakes in the HTML documentation
|
38
|
+
task :patch => :parse do
|
39
|
+
patch_methods "Tag", :top_artists, :top_tracks, :top_albums do |method|
|
40
|
+
method.parameters << Parameter.new(:tag, true, "Last.fm tag")
|
41
|
+
end
|
42
|
+
patch_methods "Group", :weekly_album_chart do |method|
|
43
|
+
method.parameters.delete_if {|p| p.name == :user}
|
44
|
+
method.parameters << Parameter.new(:group, true, "Group name")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def patch_methods(klass, *methods, &block)
|
49
|
+
REMOTE_METHODS[klass].select {|m| methods.include?(m.name) }.each do |method|
|
50
|
+
yield method
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
task :build => :patch do
|
55
|
+
KLASSES = REMOTE_METHODS.map do |klass_name, methods|
|
56
|
+
warn "Building #{klass_name}"
|
57
|
+
methods.inject(RubyClass.new(klass_name)) do |klass, method|
|
58
|
+
klass.methods << RubyMethod.new(method)
|
59
|
+
klass
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
desc "Scrapes the HTML documentation from the Last.FM site and uses it to construct ruby facade objects"
|
65
|
+
task :compile => :build do
|
66
|
+
klasses = KLASSES
|
67
|
+
File.open("lib/shortwave/facade/lastfm.rb", "w") do |fh|
|
68
|
+
fh.write ERB.new(File.read("lib/shortwave/facade/build/facade_template.erb")).result(binding)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
rescue LoadError
|
74
|
+
warn "Cannot build a fresh Facade::Remote - missing gems (nokogiri)?"
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
begin
|
79
|
+
require 'jeweler'
|
80
|
+
Jeweler::Tasks.new do |gem|
|
81
|
+
gem.name = "shortwave"
|
82
|
+
gem.summary = "A Last.fm API wrapper"
|
83
|
+
gem.email = "roland.swingler@gmail.com"
|
84
|
+
gem.homepage = "http://shortwave.rubyforge.org"
|
85
|
+
gem.authors = ["Roland Swingler"]
|
86
|
+
gem.add_dependency("rest-client", ">= 0.9.2")
|
87
|
+
gem.add_dependency("nokogiri", ">= 1.2.3")
|
88
|
+
gem.rubyforge_project = "shortwave"
|
89
|
+
end
|
90
|
+
rescue LoadError
|
91
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
92
|
+
end
|
93
|
+
|
94
|
+
require 'rake/testtask'
|
95
|
+
Rake::TestTask.new(:test) do |test|
|
96
|
+
test.libs << 'lib' << 'test'
|
97
|
+
test.pattern = 'test/**/*_test.rb'
|
98
|
+
test.verbose = true
|
99
|
+
end
|
100
|
+
|
101
|
+
begin
|
102
|
+
require 'rcov/rcovtask'
|
103
|
+
Rcov::RcovTask.new do |test|
|
104
|
+
test.libs << 'test'
|
105
|
+
test.pattern = 'test/**/*_test.rb'
|
106
|
+
test.verbose = true
|
107
|
+
end
|
108
|
+
rescue LoadError
|
109
|
+
task :rcov do
|
110
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
task :default => :test
|
116
|
+
|
117
|
+
require 'rake/rdoctask'
|
118
|
+
Rake::RDocTask.new do |rdoc|
|
119
|
+
if File.exist?('VERSION.yml')
|
120
|
+
config = YAML.load(File.read('VERSION.yml'))
|
121
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
122
|
+
else
|
123
|
+
version = ""
|
124
|
+
end
|
125
|
+
|
126
|
+
rdoc.rdoc_dir = 'doc'
|
127
|
+
rdoc.title = "shortwave #{version}"
|
128
|
+
rdoc.rdoc_files.include('README*')
|
129
|
+
rdoc.rdoc_files.include('lib/*.rb')
|
130
|
+
rdoc.rdoc_files.include('lib/shortwave/*.rb')
|
131
|
+
rdoc.rdoc_files.include('lib/shortwave/facade/*.rb')
|
132
|
+
rdoc.rdoc_files.include('lib/shortwave/model/*.rb')
|
133
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/lib/shortwave.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + "/shortwave")
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + "/../vendor/happymapper/lib")
|
3
|
+
require 'happymapper'
|
4
|
+
require 'authentication'
|
5
|
+
require 'facade'
|
6
|
+
require 'providers'
|
7
|
+
require 'model/base_model'
|
8
|
+
Dir[File.dirname(__FILE__) + "/shortwave/model/*.rb"].each {|model| require model }
|
9
|
+
|
10
|
+
module Shortwave
|
11
|
+
Authentication::Session.send(:include, Provider::ProviderMethods)
|
12
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'digest/md5'
|
3
|
+
include Digest
|
4
|
+
|
5
|
+
module Shortwave
|
6
|
+
# Authentication classes.
|
7
|
+
#
|
8
|
+
# See http://www.last.fm/api/authentication for more details on how Last.fm expects
|
9
|
+
# you to authenticate. The Sessions take care of method signatures and so on for
|
10
|
+
# you.
|
11
|
+
module Authentication
|
12
|
+
# Indicates that you have tried to call a remote method that require authentication,
|
13
|
+
# but have not authenticated yet.
|
14
|
+
class NotAuthenticated < StandardError
|
15
|
+
end
|
16
|
+
|
17
|
+
# Base functionality for session-based authentication mechanisms. Don't use this
|
18
|
+
# directly - use one of its subclasses: Web, Desktop or Mobile.
|
19
|
+
class Session
|
20
|
+
attr_reader :session_key
|
21
|
+
|
22
|
+
# Creates a new session with your api account key and secret. If you have already
|
23
|
+
# authenticated and stored a session key, you can provide it to save having to
|
24
|
+
# authenticate again.
|
25
|
+
def initialize(api_key, secret, session_key=nil)
|
26
|
+
@api_key, @secret, @session_key = api_key, secret, session_key
|
27
|
+
@facade = Facade::Auth.new(self)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Is the user signed in to Last.fm?
|
31
|
+
def signed_in?
|
32
|
+
! @session_key.nil?
|
33
|
+
end
|
34
|
+
|
35
|
+
# Merges relevant authentication details with method parameters.
|
36
|
+
def merge!(type, params)
|
37
|
+
raise NotAuthenticated.new("Requires authentication!") if type == :session && @session_key.nil?
|
38
|
+
|
39
|
+
params.merge!(:api_key => @api_key)
|
40
|
+
params.merge!(:sk => @session_key) if type == :session
|
41
|
+
params.merge!(:api_sig => signature(params)) if type == :session || type == :signed
|
42
|
+
params
|
43
|
+
end
|
44
|
+
|
45
|
+
# Generates a method signature for method parameters.
|
46
|
+
def signature(params)
|
47
|
+
sorted_params = params.map {|k,v| [k.to_s, v.to_s] }.sort_by {|a| a[0] }
|
48
|
+
MD5.hexdigest(sorted_params.flatten.join("") + @secret)
|
49
|
+
end
|
50
|
+
|
51
|
+
protected
|
52
|
+
|
53
|
+
# Parses the response to Auth.getSession
|
54
|
+
def parse_session_response(response)
|
55
|
+
doc = Nokogiri::XML(response)
|
56
|
+
if doc.root['status'] = 'ok'
|
57
|
+
@session_key = doc.css("key").text.strip
|
58
|
+
else
|
59
|
+
raise doc.css("error").text
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Authentication for mobile applications. Don't use this for web/desktop applications
|
65
|
+
# use either Authentication::Web or Authentication::Desktop instead
|
66
|
+
class Mobile < Session
|
67
|
+
# Authenticates with a user's username and password
|
68
|
+
def authenticate(username, password)
|
69
|
+
response = @facade.mobile_session(username, MD5.hexdigest(username + MD5.hexdigest(password)))
|
70
|
+
parse_session_response(response)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Authentication for web applications. Send your user to the page given by +uri+
|
75
|
+
# and use the token provided to your callback url as an argument to +authenticate+
|
76
|
+
class Web < Session
|
77
|
+
# The uri you should direct users to in their web browser, so they can authenticate. If successful,
|
78
|
+
# the callback url defined in your api account will be called, with a token parameter. Pass this
|
79
|
+
# token to the authenticate method.
|
80
|
+
def uri
|
81
|
+
"http://www.last.fm/api/auth/?api_key=#{@api_key}"
|
82
|
+
end
|
83
|
+
|
84
|
+
# Gets an authenticated session key. Call after the user has logged in at +uri+ with the
|
85
|
+
# token returned to your callback uri.
|
86
|
+
def authenticate(token)
|
87
|
+
parse_session_response(@facade.session(token))
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Authentication for destop applications. Send your user to the page given by +uri+
|
92
|
+
# and then call +authenticate+.
|
93
|
+
class Desktop < Session
|
94
|
+
# A uri the user should log in at.
|
95
|
+
def uri
|
96
|
+
response = @facade.token
|
97
|
+
@token = Nokogiri::XML(response).css("token").text.strip
|
98
|
+
"http://www.last.fm/api/auth/?api_key=#{@api_key}&token=#{@token}"
|
99
|
+
end
|
100
|
+
|
101
|
+
# Gets an authenticated session key. Call after the user has logged in at +uri+.
|
102
|
+
def authenticate
|
103
|
+
parse_session_response(@facade.session(token))
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'httparty'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Shortwave
|
6
|
+
module Facade
|
7
|
+
module Build
|
8
|
+
METHOD_NAME_CSS = "#wstitle ~ h1"
|
9
|
+
PARAMETER_CSS = "#wsdescriptor h2 ~ .param"
|
10
|
+
SAMPLE_CSS = "#sample pre"
|
11
|
+
DESCRIPTION_CSS = ".wsdescription"
|
12
|
+
METHOD_TYPE_CSS = "#wsdescriptor"
|
13
|
+
REMOTE_CLASS = "li.package"
|
14
|
+
|
15
|
+
COMBINED_PARAMS = /^(.+)\[(.+)\]$/
|
16
|
+
PARAMETER_TEXT = /\s*\(([^\)]+)\)\s*:\s*(.*)/
|
17
|
+
IN_WORD_CAPS = /(.)([A-Z])/
|
18
|
+
STARTS_WITH_GET = /^.+\.(get)?/
|
19
|
+
|
20
|
+
|
21
|
+
# Various Helper methods that belong on String
|
22
|
+
module StringExtensions
|
23
|
+
# Convert a string like FooBar to foo_bar
|
24
|
+
def camel_to_snake
|
25
|
+
gsub(IN_WORD_CAPS,"\\1_\\2").tr('A-Z','a-z')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
String.send(:include, StringExtensions)
|
29
|
+
|
30
|
+
|
31
|
+
# Helper methods for constructing the Facades
|
32
|
+
class FacadeBuilder
|
33
|
+
def remote_method_definitions(location)
|
34
|
+
return @method_definitions if @method_definitions
|
35
|
+
if File.exists?( location )
|
36
|
+
@method_definitions = YAML.load(File.read(location))
|
37
|
+
else
|
38
|
+
response = Build::DocumentationRemote.scrape_remote_method_index
|
39
|
+
File.open(location, "w") {|fh| fh.write(response.to_yaml) }
|
40
|
+
@method_definitions = response
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# A remote facade for the Last FM html documentation
|
46
|
+
class DocumentationRemote
|
47
|
+
include HTTParty
|
48
|
+
base_uri "http://last.fm"
|
49
|
+
|
50
|
+
def self.scrape_remote_method_index
|
51
|
+
html = get("/api/intro")
|
52
|
+
Nokogiri::HTML(html).css(REMOTE_CLASS).inject({}) do |hsh, node|
|
53
|
+
hsh[node.text] = node.next.next.css("a").inject({}) do |h, anchor|
|
54
|
+
h[anchor.text] = anchor["href"]
|
55
|
+
h
|
56
|
+
end
|
57
|
+
hsh
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
# The class of a RemoteFacade to be generated
|
64
|
+
class RubyClass
|
65
|
+
attr_reader :name, :methods
|
66
|
+
|
67
|
+
def initialize(name)
|
68
|
+
@name = name
|
69
|
+
@methods = []
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
# A ruby method in a RemoteFacade that will be generated
|
75
|
+
class RubyMethod
|
76
|
+
attr_accessor :signature
|
77
|
+
attr_reader :comment, :body, :name
|
78
|
+
|
79
|
+
def initialize(node)
|
80
|
+
@comment = []
|
81
|
+
@body = []
|
82
|
+
@node = node
|
83
|
+
@parameters = node.parameters || []
|
84
|
+
@required, @optional = (@parameters).partition {|p| p.required? }
|
85
|
+
@required.reject! {|p| [:api_key, :api_sig, :sk].include?(p.name) }
|
86
|
+
|
87
|
+
build_comment
|
88
|
+
build_signature
|
89
|
+
build_body
|
90
|
+
self
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def build_body
|
96
|
+
mode = if @parameters.any? {|p| p.name == :sk }
|
97
|
+
:session
|
98
|
+
elsif @parameters.any? {|p| p.name == :api_sig }
|
99
|
+
:signed
|
100
|
+
else
|
101
|
+
:standard
|
102
|
+
end
|
103
|
+
|
104
|
+
get_line = "#{@node.http_method}(:#{mode}, {:method => \"#{@node.remote_name}\""
|
105
|
+
@required.each {|p| get_line << ", :#{p.name} => #{p.name}" }
|
106
|
+
get_line << "}"
|
107
|
+
get_line << ".merge(options)" unless @optional.empty?
|
108
|
+
get_line << ")"
|
109
|
+
@body << get_line
|
110
|
+
end
|
111
|
+
|
112
|
+
def build_signature
|
113
|
+
@signature = "#{@node.name}"
|
114
|
+
unless @node.parameters.nil? || @node.parameters.empty?
|
115
|
+
params = @required.map {|p| p.name }
|
116
|
+
params << "options={}" unless @optional.empty?
|
117
|
+
@signature << "(" << params.join(", ") << ")"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def build_comment
|
122
|
+
comment << "# #{@node.description}" if @node.description
|
123
|
+
|
124
|
+
unless @required.empty?
|
125
|
+
comment << "#"
|
126
|
+
@required.each {|p| comment << "# +#{p.name}+:: #{p.description}" }
|
127
|
+
end
|
128
|
+
|
129
|
+
unless @optional.empty?
|
130
|
+
comment << "#"
|
131
|
+
comment << "# <b>Options</b>"
|
132
|
+
@optional.each {|p| comment << "# +#{p.name}+:: #{p.description}" }
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
# A parameter used in a Last FM api method call.
|
139
|
+
class Parameter
|
140
|
+
attr_reader :name, :description
|
141
|
+
|
142
|
+
def initialize(name, required, description)
|
143
|
+
@name, @required, @description = name, required, description.gsub(/\s+/, ' ')
|
144
|
+
end
|
145
|
+
|
146
|
+
# Returns an array of Parameters, given an HTML page from the Last FM API
|
147
|
+
# method documentation
|
148
|
+
def self.parse(html)
|
149
|
+
doc = html.kind_of?(Nokogiri::HTML::Document) ? html : Nokogiri::HTML(html)
|
150
|
+
|
151
|
+
doc.css(PARAMETER_CSS).map do |node|
|
152
|
+
name = node.text.strip
|
153
|
+
unless name.nil? || name.empty?
|
154
|
+
if match = name.match(COMBINED_PARAMS)
|
155
|
+
match[2].split("|").map {|v| make_parameter(node, match[1] + v) }
|
156
|
+
else
|
157
|
+
make_parameter(node, name)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end.flatten.compact
|
161
|
+
end
|
162
|
+
|
163
|
+
# Returns true if this parameter is required
|
164
|
+
def required?
|
165
|
+
@required
|
166
|
+
end
|
167
|
+
|
168
|
+
private
|
169
|
+
|
170
|
+
def self.make_parameter(node, name)
|
171
|
+
if match = node.next.text.match(PARAMETER_TEXT)
|
172
|
+
self.new(name.to_sym, match[1].start_with?("Required"), match[2])
|
173
|
+
else
|
174
|
+
self.new(name.to_sym, true, "")
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
|
180
|
+
# Represents an available method in the Last FM 'rest' api. Provides information
|
181
|
+
# about how to call the method.
|
182
|
+
class RemoteMethod
|
183
|
+
attr_reader :name, :remote_name, :parameters, :description, :http_method, :sample_response
|
184
|
+
|
185
|
+
# Creates a RemoteMethod, given an html page from the Last FM API documentation.
|
186
|
+
def initialize(html)
|
187
|
+
doc = Nokogiri::HTML(html)
|
188
|
+
|
189
|
+
@remote_name = doc.css(METHOD_NAME_CSS).text.strip
|
190
|
+
@name = @remote_name.sub(STARTS_WITH_GET,'').camel_to_snake.to_sym
|
191
|
+
@parameters = Parameter.parse(doc)
|
192
|
+
@description = doc.css(DESCRIPTION_CSS).text.strip.gsub(/\s+/, ' ')
|
193
|
+
@http_method = doc.css(METHOD_TYPE_CSS).text.include?("HTTP POST") ? :post : :get
|
194
|
+
@sample_response = doc.css(SAMPLE_CSS).text.strip
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|