shortwave 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (105) hide show
  1. data/.document +5 -0
  2. data/.gitignore +10 -0
  3. data/.gitmodules +3 -0
  4. data/LICENSE +20 -0
  5. data/README.rdoc +74 -0
  6. data/Rakefile +133 -0
  7. data/VERSION +1 -0
  8. data/lib/shortwave.rb +12 -0
  9. data/lib/shortwave/authentication.rb +107 -0
  10. data/lib/shortwave/facade.rb +3 -0
  11. data/lib/shortwave/facade/build/facade_builder.rb +199 -0
  12. data/lib/shortwave/facade/build/facade_template.erb +17 -0
  13. data/lib/shortwave/facade/lastfm.rb +878 -0
  14. data/lib/shortwave/facade/remote.rb +86 -0
  15. data/lib/shortwave/model/album.rb +33 -0
  16. data/lib/shortwave/model/artist.rb +62 -0
  17. data/lib/shortwave/model/base_model.rb +112 -0
  18. data/lib/shortwave/model/chart_dates.rb +15 -0
  19. data/lib/shortwave/model/comparison.rb +15 -0
  20. data/lib/shortwave/model/event.rb +55 -0
  21. data/lib/shortwave/model/group.rb +26 -0
  22. data/lib/shortwave/model/location.rb +45 -0
  23. data/lib/shortwave/model/playlist.rb +36 -0
  24. data/lib/shortwave/model/shout.rb +21 -0
  25. data/lib/shortwave/model/tag.rb +47 -0
  26. data/lib/shortwave/model/track.rb +71 -0
  27. data/lib/shortwave/model/user.rb +70 -0
  28. data/lib/shortwave/model/venue.rb +37 -0
  29. data/lib/shortwave/model/weekly_charts.rb +31 -0
  30. data/lib/shortwave/providers.rb +161 -0
  31. data/shortwave.gemspec +178 -0
  32. data/test/authentication_test.rb +64 -0
  33. data/test/build/build_test.rb +25 -0
  34. data/test/build/data/intro.yml +2 -0
  35. data/test/build/data/screens/album_addTags.html +1238 -0
  36. data/test/build/data/screens/intro_truncated.html +426 -0
  37. data/test/build/data/screens/tasteometer_compare.html +1274 -0
  38. data/test/build/data/screens/user_getLovedTracks.html +1278 -0
  39. data/test/build/data/screens/venue_search.html +1261 -0
  40. data/test/build/facade_builder_test.rb +23 -0
  41. data/test/build/parameter_test.rb +43 -0
  42. data/test/build/remote_method_test.rb +47 -0
  43. data/test/build/ruby_class_test.rb +12 -0
  44. data/test/build/ruby_method_test.rb +137 -0
  45. data/test/helper.rb +35 -0
  46. data/test/model/album_test.rb +62 -0
  47. data/test/model/artist_test.rb +103 -0
  48. data/test/model/chart_dates_test.rb +11 -0
  49. data/test/model/comparison_test.rb +18 -0
  50. data/test/model/data/album_info.xml +38 -0
  51. data/test/model/data/album_search.xml +210 -0
  52. data/test/model/data/artist_info.xml +58 -0
  53. data/test/model/data/artist_search.xml +109 -0
  54. data/test/model/data/artist_shouts.xml +546 -0
  55. data/test/model/data/artist_top_fans.xml +405 -0
  56. data/test/model/data/event_info.xml +47 -0
  57. data/test/model/data/group_members.xml +242 -0
  58. data/test/model/data/group_weekly_album_chart.xml +1754 -0
  59. data/test/model/data/group_weekly_artist_chart.xml +604 -0
  60. data/test/model/data/group_weekly_track_chart.xml +1005 -0
  61. data/test/model/data/location_events.xml +383 -0
  62. data/test/model/data/ok.xml +2 -0
  63. data/test/model/data/playlist_fetch.xml +227 -0
  64. data/test/model/data/tag_search.xml +110 -0
  65. data/test/model/data/tag_similar.xml +254 -0
  66. data/test/model/data/tag_top_albums.xml +805 -0
  67. data/test/model/data/tag_top_artists.xml +605 -0
  68. data/test/model/data/tag_top_tags.xml +1254 -0
  69. data/test/model/data/tag_top_tracks.xml +843 -0
  70. data/test/model/data/tag_weekly_chart_list.xml +57 -0
  71. data/test/model/data/tasteometer_compare.xml +54 -0
  72. data/test/model/data/track_info.xml +53 -0
  73. data/test/model/data/track_search.xml +195 -0
  74. data/test/model/data/user_chartlist.xml +90 -0
  75. data/test/model/data/user_info.xml +16 -0
  76. data/test/model/data/user_neighbours.xml +484 -0
  77. data/test/model/data/user_playlists.xml +17 -0
  78. data/test/model/data/user_recent_tracks.xml +124 -0
  79. data/test/model/data/user_recommended_artists.xml +454 -0
  80. data/test/model/data/user_shouts.xml +9 -0
  81. data/test/model/data/user_weekly_artist_chart.xml +478 -0
  82. data/test/model/data/venue_events.xml +556 -0
  83. data/test/model/data/venue_past_events.xml +1778 -0
  84. data/test/model/data/venue_search.xml +355 -0
  85. data/test/model/event_test.rb +63 -0
  86. data/test/model/group_test.rb +45 -0
  87. data/test/model/location_test.rb +25 -0
  88. data/test/model/playlist_test.rb +51 -0
  89. data/test/model/shout_test.rb +23 -0
  90. data/test/model/tag_test.rb +39 -0
  91. data/test/model/track_test.rb +67 -0
  92. data/test/model/user_test.rb +125 -0
  93. data/test/model/venue_test.rb +60 -0
  94. data/test/provider/album_provider_test.rb +26 -0
  95. data/test/provider/artist_provider_test.rb +25 -0
  96. data/test/provider/group_provider_test.rb +9 -0
  97. data/test/provider/location_provider_test.rb +9 -0
  98. data/test/provider/playlist_provider_test.rb +12 -0
  99. data/test/provider/tag_provider_test.rb +24 -0
  100. data/test/provider/track_provider_test.rb +26 -0
  101. data/test/provider/user_provider_test.rb +11 -0
  102. data/test/provider/venue_provider_test.rb +15 -0
  103. data/test/provider_test_helper.rb +27 -0
  104. data/test/remote_test.rb +26 -0
  105. metadata +209 -0
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ *~
2
+ \#*
3
+ .\#*
4
+ *.sw?
5
+ .DS_Store
6
+ coverage
7
+ rdoc
8
+ pkg
9
+ tmp
10
+ doc
data/.gitmodules ADDED
@@ -0,0 +1,3 @@
1
+ [submodule "vendor/happymapper"]
2
+ path = vendor/happymapper
3
+ url = git://github.com/knaveofdiamonds/happymapper.git
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,3 @@
1
+ require File.dirname(__FILE__) + '/authentication'
2
+ require File.dirname(__FILE__) + '/facade/remote'
3
+ require File.dirname(__FILE__) + '/facade/lastfm'
@@ -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