shortwave 0.0.1

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 (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