isbndb 1.5.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/ACKNOWLEDGEMENTS +1 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +22 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +180 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/lib/isbndb.rb +115 -0
- data/lib/isbndb/access_key_set.rb +53 -0
- data/lib/isbndb/exceptions.rb +4 -0
- data/lib/isbndb/result.rb +64 -0
- data/lib/isbndb/result_set.rb +86 -0
- data/test/helper.rb +23 -0
- data/test/test_isbndb.rb +64 -0
- metadata +129 -0
data/.document
ADDED
data/ACKNOWLEDGEMENTS
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Special thanks to Terje Tjervaag (https://github.com/terje) for giving up the gem name 'isbndb'!
|
data/Gemfile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
gem 'libxml-ruby', '>= 1.1.4'
|
4
|
+
|
5
|
+
# Include everything needed to run rake, tests, features, etc.
|
6
|
+
group :development do
|
7
|
+
gem "shoulda", ">= 0"
|
8
|
+
gem "bundler", "~> 1.0.0"
|
9
|
+
gem "jeweler", "~> 1.5.2"
|
10
|
+
gem "rcov", ">= 0"
|
11
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
git (1.2.5)
|
5
|
+
jeweler (1.5.2)
|
6
|
+
bundler (~> 1.0.0)
|
7
|
+
git (>= 1.2.5)
|
8
|
+
rake
|
9
|
+
libxml-ruby (1.1.4)
|
10
|
+
rake (0.8.7)
|
11
|
+
rcov (0.9.9)
|
12
|
+
shoulda (2.11.3)
|
13
|
+
|
14
|
+
PLATFORMS
|
15
|
+
ruby
|
16
|
+
|
17
|
+
DEPENDENCIES
|
18
|
+
bundler (~> 1.0.0)
|
19
|
+
jeweler (~> 1.5.2)
|
20
|
+
libxml-ruby (>= 1.1.4)
|
21
|
+
rcov
|
22
|
+
shoulda
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Seth Vargo
|
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.markdown
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
Ruby ISBNdb
|
2
|
+
===========
|
3
|
+
About
|
4
|
+
-----
|
5
|
+
Ruby ISBNdb is a simple, Ruby library that connects to [ISBNdb.com's Web Service](http://isbndb.com) and API. Ruby ISBNdb is written to mimic the ease of ActiveRecord and other ORM programs, without all the added hassles. It's still in beta phases, but it is almost fully functional for the basic search features of ISBNdb.
|
6
|
+
|
7
|
+
Why it's awesome
|
8
|
+
----------------
|
9
|
+
Ruby ISBNdb now uses [libxml-ruby](http://libxml.rubyforge.org/) - the fastest Ruby parser available for XML. Other parsers rely on REXML or hpricot, which are [show to be significantly slower](http://railstips.org/blog/archives/2008/08/11/parsing-xml-with-ruby/). libxml has been shown to have the fastest HTTP request AND fastest XML-parser to date!
|
10
|
+
|
11
|
+
Instead of dealing with complicated hashes and arrays, Ruby ISBNdb populates a `ResultSet` filled with `Result` objects that behave as one would expect. Simply call `@book.title` or `@author.name`! Once a `Result` object is built, it's persistent too! That means that the XML-DOM returned by ISBNdb is parsed exactly once for each request, instead of every method call like similar versions of this gem.
|
12
|
+
|
13
|
+
Version 1.5.0 now supports API-key management! The new APIKeySet supports auto-rollover - whenever one key is used up, it will automatically try the next key in the set. Once it runs out of keys, it will raise an ISBNdb::AccessKeyError. See the docs below for sample usage!
|
14
|
+
|
15
|
+
Ruby ISBNdb is under active development! More features will be coming soon!
|
16
|
+
|
17
|
+
Installation
|
18
|
+
------------
|
19
|
+
Finally got it packaged as a gem!
|
20
|
+
|
21
|
+
gem install isbndb
|
22
|
+
|
23
|
+
Alternatively, you can download the source from here and `require 'lib/isbndb'`
|
24
|
+
|
25
|
+
**Special Thanks** to:
|
26
|
+
|
27
|
+
[Terje Tjervaag](https://github.com/terje)<br />
|
28
|
+
[http://thedailyt.com](http://thedailyt.com)
|
29
|
+
|
30
|
+
for giving up the `isbndb` gem! Thank you!
|
31
|
+
|
32
|
+
Basic Setup
|
33
|
+
-----------
|
34
|
+
Simply create a query instance variable and you're on your way:
|
35
|
+
|
36
|
+
# will auto-rollover to API-KEY-2 when API-KEY-1 meets max requests
|
37
|
+
@query = ISBNdb::Query.new(["API-KEY-1", "API-KEY-2", "API-KEY-3"])
|
38
|
+
|
39
|
+
ActiveRecord-like Usage
|
40
|
+
-----------------------
|
41
|
+
Another reason why you'll love Ruby ISBNdb is it's similarity to ActiveRecord. In fact, it's *based* on ActiveRecord, so it should look similar. It's best to lead by example, so here are a few ways to search for books, authors, etc:
|
42
|
+
|
43
|
+
@query.find_book_by_isbn("978-0-9776-1663-3")
|
44
|
+
@query.find_books_by_title("Agile Development")
|
45
|
+
@query.find_author_by_name("Seth Vargo")
|
46
|
+
@query.find_publisher_by_name("Pearson")
|
47
|
+
|
48
|
+
Advanced Usage
|
49
|
+
--------------
|
50
|
+
Additionally, you can also use a more advanced syntax for complete control:
|
51
|
+
|
52
|
+
@query.find(:collection => 'books', :where => { :isbn => '978-0-9776-1663-3' })
|
53
|
+
@query.find(:collection => 'books', :where => { :author => 'Seth Vargo' }, :results => 'prices')
|
54
|
+
|
55
|
+
Options for `:collection` include **books**, **subjects**, **categories**, **authors**, and **publishers**.
|
56
|
+
|
57
|
+
If you are unfamiliar with some of these options, have a look at the [ISBNdb API](http://isbndb.com/docs/api/)
|
58
|
+
|
59
|
+
Processing Results
|
60
|
+
------------------
|
61
|
+
A `ResultSet` is nothing more than an enhanced array of `Result` objects. The easiest way to process results from Ruby ISBNdb is most easily done using the `.each` method.
|
62
|
+
|
63
|
+
results = @query.find_books_by_title("Agile Development")
|
64
|
+
results.each do |result|
|
65
|
+
puts "title: #{result.title}"
|
66
|
+
puts "isbn10: #{result.isbn}"
|
67
|
+
puts "authors: #{result.authors_text}"
|
68
|
+
end
|
69
|
+
|
70
|
+
**Note**: calling a method on a `Result` object that is `empty?`, `blank?`, or `nil?` will *always* return `nil`. This was a calculated decision so that developers can do the following:
|
71
|
+
|
72
|
+
puts "title: #{result.title}" unless result.title.nil?
|
73
|
+
|
74
|
+
versus
|
75
|
+
|
76
|
+
puts "title: #{result.title}" unless result.title.nil? || result.title.blank? || result.title.empty?
|
77
|
+
|
78
|
+
because ISBNdb.com API is generally inconsistent with respect to returning empty strings, whitespace characters, or nothing at all.
|
79
|
+
|
80
|
+
**Note**: XML-keys to method names are inversely mapped. CamelCased XML keys and attributes (like BookData or TitleLong) are converted to lowercase under_scored methods (like book_data or title_long). ALL XML keys and attributes are mapped in this way.
|
81
|
+
|
82
|
+
Pagination
|
83
|
+
----------
|
84
|
+
Ruby ISBNdb now include pagination! Pagination is based on the `ResultSet` object. The `ResultSet` object contains the methods `go_to_page`, `next_page`, and `prev_page`... Their function should not require too much explanation. Here's a basic example:
|
85
|
+
|
86
|
+
results = @query.find_books_by_title("ruby")
|
87
|
+
results.next_page.each do |result|
|
88
|
+
puts "title: #{result.title}"
|
89
|
+
end
|
90
|
+
|
91
|
+
A more realistic example - getting **all** books of a certain title:
|
92
|
+
|
93
|
+
results = @query.find_books_by_title("ruby")
|
94
|
+
while results
|
95
|
+
results.each do |result|
|
96
|
+
puts "title: #{title}"
|
97
|
+
end
|
98
|
+
|
99
|
+
results = results.next_page
|
100
|
+
end
|
101
|
+
|
102
|
+
It seems incredibly unlikely that a developer would ever use `prev_page`, but it's still there if you need it.
|
103
|
+
|
104
|
+
Because there may be cases where a developer may need a specific page, the `go_to_page` method also exists. Consider an example where you batch-process books into your own database (which is probably against Copyright laws, but you don't seem to care...):
|
105
|
+
|
106
|
+
results = @query.find_books_by_title("ruby")
|
107
|
+
results = results.go_to_page(50) # where 50 is the page number you want
|
108
|
+
|
109
|
+
**Note**: `go_to_page`, `next_page` and `prev_page` return `nil` if the `ResultSet` is out of `Result` objects. If you try something like `results.next_page.next_page`, you could get a whiny nil. Think `LinkedLists` when working with `go_to_page`, `next_page` and `prev_page`.
|
110
|
+
|
111
|
+
**BIGGER NOTE**: `go_to_page`, `next_page` and `prev_page` BOTH make a subsequent call to the API, using up one of your 500 daily request limits. Please keep this in mind!
|
112
|
+
|
113
|
+
Advanced Key Management
|
114
|
+
-----------------------
|
115
|
+
With version 1.5.0, a new AccessKeySet allows for easy key management! It's controlled through the main @query.
|
116
|
+
|
117
|
+
@access_key_set = @query.access_key_set
|
118
|
+
|
119
|
+
@access_key_set.current_key # gets the current key
|
120
|
+
@access_key_set.next_key # gets the next key
|
121
|
+
@access_key_set.next_key! # advance the pointer (equivalent to @access_key_set.current_key = @access_key_set.next_key)
|
122
|
+
@access_key_set.prev_key # gets the previous key
|
123
|
+
@access_key_set.prev_key! # advance the pointer (equivalent to @access_key_set.current_key = @access_key_set.prev_key)
|
124
|
+
@access_key_set.use_key('abc123foobar') # use and existing key (or add it if doesn't exist)
|
125
|
+
|
126
|
+
All methods will return `nil` (except `use_key`) whenever the key does not exist.
|
127
|
+
|
128
|
+
Statistics
|
129
|
+
----------
|
130
|
+
Ruby ISBNdb now supports basic statistics (from the server):
|
131
|
+
|
132
|
+
@query.keystats # => {:requests => 50, :granted => 49}
|
133
|
+
@query.keystats[:granted] # => 49
|
134
|
+
|
135
|
+
**Note**: Ironically, this information also comes from the server, so it counts as a request...
|
136
|
+
|
137
|
+
Exceptions
|
138
|
+
----------
|
139
|
+
Ruby ISBNdb could raise the following possible exceptions:
|
140
|
+
|
141
|
+
ISBNdb::AccessKeyError
|
142
|
+
ISBNdb::InvalidURIError
|
143
|
+
|
144
|
+
You will most likely encounter `ISBNdb::AccessKeyError` when you have reached your 500-request daily limit. `ISBNdb::InvalidURIError` usually occurs when using magic finder methods with typographical errors.
|
145
|
+
|
146
|
+
A Real-Life Example
|
147
|
+
-------------------
|
148
|
+
Here is a real-life example of how to use Ruby ISBNdb. Imagine a Rails application that recommends books. You have written a model, `Book`, that has a variety of methods. One of those class methods, `similar`, returns a list of book isbn values that are similar to the current book. Here's how one may do that:
|
149
|
+
|
150
|
+
# books_controller.rb
|
151
|
+
def simliar
|
152
|
+
@book = Book.find(params[:id])
|
153
|
+
@query = ISBNdb::Query.new(['API-KEY-1', 'API-KEY-2'])
|
154
|
+
@isbns = @book.similar # returns an array like [1234567890, 0987654321, 3729402827...]
|
155
|
+
|
156
|
+
@isbns.each do |isbn|
|
157
|
+
begin
|
158
|
+
(@books ||= []) << @query.find_book_by_isbn(isbn).first
|
159
|
+
rescue ISBNdb::AccessKeyError
|
160
|
+
SomeMailer.send_limit_email.deliver!
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# similar.html.erb
|
166
|
+
<h1>The following books are recommeded for you:</h1>
|
167
|
+
<% @books.each do |book| %>
|
168
|
+
<div class="book">
|
169
|
+
<h2><%= book.title_long %></h2>
|
170
|
+
<p><strong>authors</strong>: <%= book.authors_text %></p>
|
171
|
+
</div>
|
172
|
+
<% end %>
|
173
|
+
|
174
|
+
Know Bugs and Limitations
|
175
|
+
-------------------------
|
176
|
+
- Result sets that return multiple sub-lists (like prices, pricehistory, and authors) are only populated with the *last* result
|
177
|
+
|
178
|
+
Change Log
|
179
|
+
----------
|
180
|
+
2011-3-11 - Officially changed from ruby_isbndb to isbndb with special thanks to [Terje Tjervaag](https://github.com/terje) for giving up the gem name :)
|
data/Rakefile
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'rake'
|
11
|
+
|
12
|
+
require 'jeweler'
|
13
|
+
Jeweler::Tasks.new do |gem|
|
14
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
15
|
+
gem.name = "isbndb"
|
16
|
+
gem.homepage = "http://github.com/svargo/isbndb"
|
17
|
+
gem.license = "MIT"
|
18
|
+
gem.summary = "This gem provides an easy solution for connecting to ISBNdb.com's API"
|
19
|
+
gem.description = "Ruby ISBNdb is a amazingly fast and accurate gem that reads ISBNdb.com's XML API and gives you incredible flexibilty with the results! The gem uses libxml-ruby, the fastest XML parser for Ruby, so you get blazing fast results every time. Additionally, the newest version of the gem also features caching, so developers minimize API-key usage."
|
20
|
+
gem.email = "seth.vargo@gmail.com"
|
21
|
+
gem.authors = ["Seth Vargo"]
|
22
|
+
end
|
23
|
+
Jeweler::RubygemsDotOrgTasks.new
|
24
|
+
|
25
|
+
require 'rake/testtask'
|
26
|
+
Rake::TestTask.new(:test) do |test|
|
27
|
+
test.libs << 'lib' << 'test'
|
28
|
+
test.pattern = 'test/**/test_*.rb'
|
29
|
+
test.verbose = true
|
30
|
+
end
|
31
|
+
|
32
|
+
require 'rcov/rcovtask'
|
33
|
+
Rcov::RcovTask.new do |test|
|
34
|
+
test.libs << 'test'
|
35
|
+
test.pattern = 'test/**/test_*.rb'
|
36
|
+
test.verbose = true
|
37
|
+
end
|
38
|
+
|
39
|
+
task :default => :test
|
40
|
+
|
41
|
+
require 'rake/rdoctask'
|
42
|
+
Rake::RDocTask.new do |rdoc|
|
43
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
44
|
+
|
45
|
+
rdoc.rdoc_dir = 'rdoc'
|
46
|
+
rdoc.title = "ruby_isbndb #{version}"
|
47
|
+
rdoc.rdoc_files.include('README*')
|
48
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
49
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.5.3
|
data/lib/isbndb.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
# require dependencies
|
2
|
+
require 'libxml'
|
3
|
+
|
4
|
+
# private sub-classes
|
5
|
+
require 'isbndb/access_key_set'
|
6
|
+
require 'isbndb/exceptions'
|
7
|
+
require 'isbndb/result_set'
|
8
|
+
require 'isbndb/result'
|
9
|
+
|
10
|
+
module ISBNdb
|
11
|
+
# The Query object is the most important class of the ISBNdb Module. It is the only public
|
12
|
+
# class, and handles the processing power.
|
13
|
+
class Query
|
14
|
+
DEFAULT_COLLECTION = :books
|
15
|
+
DEFAULT_RESULTS = :details
|
16
|
+
BASE_URL = "http://isbndb.com/api"
|
17
|
+
|
18
|
+
# Access methods of the access_key_set instance variable. This allows developers to manually
|
19
|
+
# advance, add, remove, and manage keys. See the AccessKeySet class for more information.
|
20
|
+
attr_reader :access_key_set
|
21
|
+
|
22
|
+
# This method sets an array of access_keys to use for making requests to the ISBNdb API.
|
23
|
+
def initialize(access_keys)
|
24
|
+
@access_key_set = ISBNdb::AccessKeySet.new(access_keys)
|
25
|
+
end
|
26
|
+
|
27
|
+
# This is the generic find method. It accepts a hash of parameters including :collection,
|
28
|
+
# :where clauses, and :results to show. It builds the corresponding URI and sends that URI
|
29
|
+
# off to the ResultSet for processing.
|
30
|
+
def find(params = {})
|
31
|
+
raise "No parameters specified! You must specify at least one parameter!" unless params[:where]
|
32
|
+
|
33
|
+
collection = params[:collection] ||= DEFAULT_COLLECTION
|
34
|
+
results = params[:results] ||= DEFAULT_RESULTS
|
35
|
+
results = [results].flatten
|
36
|
+
|
37
|
+
# build the search clause
|
38
|
+
searches = []
|
39
|
+
params[:where].each_with_index do |(key,val), i|
|
40
|
+
searches << "index#{i+1}=#{key.to_s.strip}"
|
41
|
+
searches << "value#{i+1}=#{val.to_s.strip}"
|
42
|
+
end
|
43
|
+
|
44
|
+
# make the request
|
45
|
+
make_request(collection, results, searches)
|
46
|
+
end
|
47
|
+
|
48
|
+
# This method returns keystats about your API key, including the number of requests
|
49
|
+
# and the number of granted requets. Be advised that this request actually counts
|
50
|
+
# as a request to the server, so use with caution.
|
51
|
+
def keystats
|
52
|
+
uri = "#{BASE_URL}/books.xml?access_key=#{@access_key_set.current_key}&results=keystats"
|
53
|
+
keystats = {}
|
54
|
+
LibXML::XML::Parser.file(uri).parse.find('KeyStats').first.attributes.each { |attribute| keystats[attribute.name.to_sym] = attribute.value.to_i unless attribute.name == 'access_key' }
|
55
|
+
return keystats
|
56
|
+
end
|
57
|
+
|
58
|
+
# Method missing allows for dynamic finders, similar to that of ActiveRecord. See
|
59
|
+
# the README for more information on using magic finders.
|
60
|
+
def method_missing(m, *args, &block)
|
61
|
+
m = m.to_s.downcase
|
62
|
+
|
63
|
+
if m.match(/find_(.+)_by_(.+)/)
|
64
|
+
split = m.split('_', 4)
|
65
|
+
collection, search_strs = pluralize(split[1].downcase), [split.last]
|
66
|
+
|
67
|
+
# check and see if we are searching multiple fields
|
68
|
+
search_strs = search_strs.first.split('_and_') if(search_strs.first.match(/_and_/))
|
69
|
+
raise "Wrong Number of Arguments (#{args.size} for #{search_strs.size})" if args.size != search_strs.size
|
70
|
+
|
71
|
+
# create the searches hash
|
72
|
+
searches = {}
|
73
|
+
search_strs.each_with_index { |str, i| searches[str.strip.to_sym] = args[i].strip }
|
74
|
+
|
75
|
+
return find(:collection => collection, :where => searches)
|
76
|
+
end
|
77
|
+
|
78
|
+
super
|
79
|
+
end
|
80
|
+
|
81
|
+
# Pretty print the Query object with the access key.
|
82
|
+
def to_s
|
83
|
+
"#<ISBNdb::Query, @access_key=#{@access_key_set.current_key}>"
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
# Make the request to the ResultSet. If the request fails because of an ISBNdb::AccessKeyError
|
88
|
+
# the system will automatically rollover to the next AccessKey in the AccessKeySet. If one exists,
|
89
|
+
# a new request is attempted. If not, the ISBNdb::AccessKeyError persists and can be caught by your
|
90
|
+
# application logic.
|
91
|
+
def make_request(collection, results, searches)
|
92
|
+
begin
|
93
|
+
uri = "#{BASE_URL}/#{collection}.xml?access_key=#{@access_key_set.current_key}&results=#{results.join(',')}&#{searches.join('&')}"
|
94
|
+
ISBNdb::ResultSet.new(uri, singularize(collection).capitalize)
|
95
|
+
rescue ISBNdb::AccessKeyError
|
96
|
+
puts "Access Key Error (#{@access_key_set.current_key}) - You probably reached your limit! Trying the next key."
|
97
|
+
@access_key_set.next_key!
|
98
|
+
retry unless @access_key_set.current_key.nil?
|
99
|
+
raise ISBNdb::AccessKeyError
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def pluralize(str)
|
104
|
+
return 'categories' if str == 'category'
|
105
|
+
return "#{str}s" unless str.split(//).last == 's'
|
106
|
+
str
|
107
|
+
end
|
108
|
+
|
109
|
+
def singularize(str)
|
110
|
+
return 'category' if str == 'categories'
|
111
|
+
return str[0, str.length-1] if str.split(//).last == 's'
|
112
|
+
str
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module ISBNdb
|
2
|
+
|
3
|
+
private
|
4
|
+
# The AccessKeySet is a simple class used to manage access keys. It is used primarily
|
5
|
+
# by the ruby_isbndb class to automatically advance access keys when necessary.
|
6
|
+
class AccessKeySet
|
7
|
+
# Create the @access_keys array and then verify that the keys are valid keys.
|
8
|
+
def initialize(access_keys)
|
9
|
+
@access_keys = [access_keys].flatten
|
10
|
+
end
|
11
|
+
|
12
|
+
# Get the current key. It returns a string of the access key.
|
13
|
+
def current_key
|
14
|
+
@access_keys[@current_index ||= 0]
|
15
|
+
end
|
16
|
+
|
17
|
+
# Move the key pointer forward.
|
18
|
+
def next_key!
|
19
|
+
@current_index += 1
|
20
|
+
end
|
21
|
+
|
22
|
+
# Get the next key.
|
23
|
+
def next_key
|
24
|
+
@access_keys[@current_index+1]
|
25
|
+
end
|
26
|
+
|
27
|
+
# Move the key pointer back.
|
28
|
+
def prev_key!
|
29
|
+
@current_index -= 1
|
30
|
+
end
|
31
|
+
|
32
|
+
# Get the previous key.
|
33
|
+
def prev_key
|
34
|
+
@access_keys[@current_index-1]
|
35
|
+
end
|
36
|
+
|
37
|
+
# Tell Ruby ISBNdb to use a specified key. If the key does not exist, it is
|
38
|
+
# added to the set and set as the current key.
|
39
|
+
def use_key(key)
|
40
|
+
@current_index = @access_keys.index(key) || @access_keys.push(key).index(key)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Remove the given access key from the AccessKeySet.
|
44
|
+
def remove_key(key)
|
45
|
+
@access_keys.delete(key)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Pretty print the AccessKeySet
|
49
|
+
def to_s
|
50
|
+
"#<AccessKeySet @keys=<#{@access_keys.collect{ |key| key }.join(',')}>>"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module ISBNdb
|
2
|
+
|
3
|
+
private
|
4
|
+
# The Result object is a true testament of metaprogramming. Almost every method of the Result
|
5
|
+
# is dynamically generated through the build_result() method. All attribtues of the XML are
|
6
|
+
# parsed, translated, and populated as instance methods on the Result object. This allows for
|
7
|
+
# easy Ruby-like access (@book.title), without hardcoding every single possible return value
|
8
|
+
# from the ISBNdb API
|
9
|
+
class Result
|
10
|
+
# Initialize simply calls build_result. Because the method definition is recusive, it must
|
11
|
+
# be moved into a separate helper.
|
12
|
+
def initialize(top_node)
|
13
|
+
build_result(top_node)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Because a result may or may not contain a specified key, we always return nil for
|
17
|
+
# consistency. This allows developers to easily check for .nil? instead of checking for
|
18
|
+
# a miriad of exceptions throughout their code.
|
19
|
+
def method_missing(m, *args, &block)
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
|
23
|
+
# Pretty preint the Result including the number of singleton methods that exist. If
|
24
|
+
# you want the ACTUAL singleton methods, call @result.singleton_methods.
|
25
|
+
def to_s
|
26
|
+
"#<Result @singleton_methods=#{@singleton_methods.size}>"
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
# This is the `magical` method. It essentially parses each attribute of the XML as well as
|
31
|
+
# the content of each XML node, dynamically sends a method to the instance with that attribute's
|
32
|
+
# or content's value. Not to be outdone, it recursively iterates over all children too!
|
33
|
+
def build_result(top_node)
|
34
|
+
top_node.attributes.each do |attribute|
|
35
|
+
singleton.send(:define_method, formatted_method_name(attribute.name)) { attribute.value } unless attribute.value.strip.empty?
|
36
|
+
end
|
37
|
+
|
38
|
+
if top_node.children?
|
39
|
+
top_node.children.each { |child| build_result(child) }
|
40
|
+
else
|
41
|
+
singleton.send(:define_method, formatted_method_name(top_node.parent.name)) { top_node.content.strip.chomp(',') } unless top_node.content.strip.empty?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# This helper function reduces code redundancy and maintains consistency by formatting
|
46
|
+
# all method names the same. All method names are stripped of any trailing whitespaces,
|
47
|
+
# converted from CamelCase to under_score, and converted to a symbol
|
48
|
+
def formatted_method_name(name)
|
49
|
+
camel_to_underscore(name.strip).to_sym
|
50
|
+
end
|
51
|
+
|
52
|
+
# This helper function converts CamelCase to under_score using a nice little regex :).
|
53
|
+
def camel_to_underscore(str)
|
54
|
+
str.gsub(/(.)([A-Z])/,'\1_\2').downcase
|
55
|
+
end
|
56
|
+
|
57
|
+
# We need a singleton reference to the current _instance_ so that we can dynamically define
|
58
|
+
# methods. This is just a simple helper that returns the singleton class of the current
|
59
|
+
# object instance.
|
60
|
+
def singleton
|
61
|
+
class << self; self end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module ISBNdb
|
2
|
+
|
3
|
+
private
|
4
|
+
# The ResultSet is a collection of Result objects with helper methods for pagination. It
|
5
|
+
# allows for easy paginating through multiple pages of results as well as jumping to a
|
6
|
+
# specific results page.
|
7
|
+
class ResultSet
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
# This method creates instance variables for @uri, @collection, and @current_page. It then
|
11
|
+
# attempts to parse the XML at the gieven URI. If it cannot parse the URI for any reason,
|
12
|
+
# it will raise an ISBNdb::InvalidURIError. Next, the results are checked for an any error
|
13
|
+
# messages. An ISBNdb::AccessKeyError will be raised if the results contain any errors.
|
14
|
+
# Finally, this method then actually builds the ResultSet.
|
15
|
+
def initialize(uri, collection, current_page = 1)
|
16
|
+
@uri = uri
|
17
|
+
@collection = collection
|
18
|
+
@current_page = current_page
|
19
|
+
@xml = parse_xml
|
20
|
+
|
21
|
+
check_results
|
22
|
+
build_results
|
23
|
+
end
|
24
|
+
|
25
|
+
# Because ResultSet extends Enumerable, we need to define the each method. This allows users
|
26
|
+
# to call methods like .first, .last, [5], and .each on the ResultSet, making it behave like
|
27
|
+
# a primitive array.
|
28
|
+
def each(&block)
|
29
|
+
@results.each &block
|
30
|
+
end
|
31
|
+
|
32
|
+
# Jump to a specific page. This method will return nil if the specified page does not exist.
|
33
|
+
def go_to_page(page)
|
34
|
+
get_total_pages unless @total_pages
|
35
|
+
return nil if page < 1 || page > @total_pages
|
36
|
+
ISBNdb::ResultSet.new("#{@uri}&page_number=#{page}", @collection, page)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Go to the next page. This method will return nil if a next page does not exist.
|
40
|
+
def next_page
|
41
|
+
go_to_page(@current_page+1)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Go to the previous page. This method will return nil if a previous page does not exist.
|
45
|
+
def prev_page
|
46
|
+
go_to_page(@current_page-1)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Pretty prints the Result set information.
|
50
|
+
def to_s
|
51
|
+
"#<ResultSet @collection=#{@collection}, total_results=#{@results.size}>"
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
# Try and parses and returns the XML from the given URI. If the parsing fails for any reason, this method
|
56
|
+
# raises ISBNdb::InvalidURIError.
|
57
|
+
def parse_xml
|
58
|
+
begin
|
59
|
+
LibXML::XML::Parser.file(@uri).parse
|
60
|
+
rescue
|
61
|
+
raise ISBNdb::InvalidURIError
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Check the results for an error message. If one exists, raise an ISBNdb::AccessKeyError for now.
|
66
|
+
# Currently the API does not differentiate between an overloaded API key and an invalid one
|
67
|
+
# (it returns the same exact response), so there only exists one exception for now...
|
68
|
+
def check_results
|
69
|
+
raise ISBNdb::AccessKeyError unless @xml.find("ErrorMessage").first.nil?
|
70
|
+
end
|
71
|
+
|
72
|
+
# Iterate over #{@collection}List/#{@collection}Data (ex. BookList/BookData) and build a result with
|
73
|
+
# each child. This method works because the API always returns #{@collection}List followed by a subset
|
74
|
+
# of #{@collection}Data. These results are all pushed into the @results array for accessing.
|
75
|
+
def build_results
|
76
|
+
@xml.find("#{@collection}List/#{@collection}Data").collect { |node| (@results ||= []) << Result.new(node) }
|
77
|
+
end
|
78
|
+
|
79
|
+
# This helper method is mainly designed for use with the go_to_page(page) method. It parses the XML
|
80
|
+
# and returns the total number of pages that exist for this result set.
|
81
|
+
def get_total_pages
|
82
|
+
list = @xml.find("#{@collection}List").first.attributes
|
83
|
+
@total_pages = (list['total_results'].to_f/list['page_size'].to_f).ceil
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
# Set up gems listed in the Gemfile.
|
3
|
+
gemfile = File.expand_path('../../Gemfile', __FILE__)
|
4
|
+
begin
|
5
|
+
ENV['BUNDLE_GEMFILE'] = gemfile
|
6
|
+
require 'bundler'
|
7
|
+
Bundler.setup
|
8
|
+
rescue Bundler::GemNotFound => e
|
9
|
+
STDERR.puts e.message
|
10
|
+
STDERR.puts "Try running `bundle install`."
|
11
|
+
exit!
|
12
|
+
end if File.exist?(gemfile)
|
13
|
+
|
14
|
+
|
15
|
+
require 'test/unit'
|
16
|
+
require 'shoulda'
|
17
|
+
|
18
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
19
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
20
|
+
require 'isbndb'
|
21
|
+
|
22
|
+
class Test::Unit::TestCase
|
23
|
+
end
|
data/test/test_isbndb.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestISBNdb < Test::Unit::TestCase
|
4
|
+
context "ISBNdb" do
|
5
|
+
setup do
|
6
|
+
@query = ISBNdb::Query.new('KXWFXJIK')
|
7
|
+
end
|
8
|
+
|
9
|
+
should "fetch a book by ISBN" do
|
10
|
+
@book = @query.find_book_by_isbn('1934356549').first
|
11
|
+
assert_equal '1934356549', @book.isbn
|
12
|
+
end
|
13
|
+
|
14
|
+
should "fetch a book by ISBN13" do
|
15
|
+
@book = @query.find_book_by_isbn('9781934356548').first
|
16
|
+
assert_equal '9781934356548', @book.isbn13
|
17
|
+
end
|
18
|
+
|
19
|
+
should 'fetch books by title' do
|
20
|
+
@books = @query.find_books_by_title('ruby')
|
21
|
+
@books.each do |book|
|
22
|
+
assert (book.title || "").downcase.include?('ruby') || (book.title_long || "").downcase.include?('ruby'), "#{book.title} did not contain 'ruby'"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
should 'get next_page and prev_page' do
|
27
|
+
@books = @query.find_books_by_title('ruby')
|
28
|
+
next_page = @books.next_page
|
29
|
+
assert_equal @books.first.title, next_page.prev_page.first.title, 'Failed to get next_page'
|
30
|
+
end
|
31
|
+
|
32
|
+
should 'get keystats' do
|
33
|
+
assert @query.keystats.is_a?(Hash), "#{@query.keystats} was not a Hash"
|
34
|
+
assert @query.keystats[:requests] >= @query.keystats[:granted], "Number of requests (#{@query.keystats[:requests]}) was not greater than number of granted requests (#{@query.keystats[:granted]})"
|
35
|
+
assert @query.keystats[:requests] > 0 && @query.keystats[:requests] < 500, 'Requests were not between 0 and 500'
|
36
|
+
end
|
37
|
+
|
38
|
+
should 'test access_key_set' do
|
39
|
+
@query = ISBNdb::Query.new(['API-KEY-1', 'API-KEY-2', 'API-KEY-3'])
|
40
|
+
@access_key_set = @query.access_key_set
|
41
|
+
assert_equal 'API-KEY-1', @access_key_set.current_key
|
42
|
+
assert_equal 'API-KEY-2', @access_key_set.next_key
|
43
|
+
@access_key_set.next_key!
|
44
|
+
assert_equal 'API-KEY-1', @access_key_set.prev_key
|
45
|
+
@access_key_set.use_key('A-NEW-KEY')
|
46
|
+
assert_equal 'A-NEW-KEY', @access_key_set.current_key
|
47
|
+
@access_key_set.use_key('API-KEY-3')
|
48
|
+
assert_equal 'API-KEY-3', @access_key_set.current_key
|
49
|
+
end
|
50
|
+
|
51
|
+
should 'raise exception for an invalid access key' do
|
52
|
+
@invalid = ISBNdb::Query.new(['abc123foobar', '123anotherinvalidkey'])
|
53
|
+
assert_raise ISBNdb::AccessKeyError, "#{@invalid} did not raise AccessKeyError" do
|
54
|
+
@invalid.find_book_by_isbn('1934356549')
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
should 'raise exception for an invalid uri' do
|
59
|
+
assert_raise ISBNdb::InvalidURIError, "@query.find_invalid_by_unknown did not raise InvalidURIError" do
|
60
|
+
@query.find_invalid_by_unknown('foobar')
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
metadata
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: isbndb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 1.5.3
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Seth Vargo
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-03-11 00:00:00 -05:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: libxml-ruby
|
18
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.1.4
|
24
|
+
type: :runtime
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: shoulda
|
29
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
30
|
+
none: false
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: "0"
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: *id002
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: bundler
|
40
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 1.0.0
|
46
|
+
type: :development
|
47
|
+
prerelease: false
|
48
|
+
version_requirements: *id003
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: jeweler
|
51
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ~>
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: 1.5.2
|
57
|
+
type: :development
|
58
|
+
prerelease: false
|
59
|
+
version_requirements: *id004
|
60
|
+
- !ruby/object:Gem::Dependency
|
61
|
+
name: rcov
|
62
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: "0"
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: *id005
|
71
|
+
description: Ruby ISBNdb is a amazingly fast and accurate gem that reads ISBNdb.com's XML API and gives you incredible flexibilty with the results! The gem uses libxml-ruby, the fastest XML parser for Ruby, so you get blazing fast results every time. Additionally, the newest version of the gem also features caching, so developers minimize API-key usage.
|
72
|
+
email: seth.vargo@gmail.com
|
73
|
+
executables: []
|
74
|
+
|
75
|
+
extensions: []
|
76
|
+
|
77
|
+
extra_rdoc_files:
|
78
|
+
- LICENSE.txt
|
79
|
+
- README.markdown
|
80
|
+
files:
|
81
|
+
- .document
|
82
|
+
- ACKNOWLEDGEMENTS
|
83
|
+
- Gemfile
|
84
|
+
- Gemfile.lock
|
85
|
+
- LICENSE.txt
|
86
|
+
- README.markdown
|
87
|
+
- Rakefile
|
88
|
+
- VERSION
|
89
|
+
- lib/isbndb.rb
|
90
|
+
- lib/isbndb/access_key_set.rb
|
91
|
+
- lib/isbndb/exceptions.rb
|
92
|
+
- lib/isbndb/result.rb
|
93
|
+
- lib/isbndb/result_set.rb
|
94
|
+
- test/helper.rb
|
95
|
+
- test/test_isbndb.rb
|
96
|
+
has_rdoc: true
|
97
|
+
homepage: http://github.com/svargo/isbndb
|
98
|
+
licenses:
|
99
|
+
- MIT
|
100
|
+
post_install_message:
|
101
|
+
rdoc_options: []
|
102
|
+
|
103
|
+
require_paths:
|
104
|
+
- lib
|
105
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
hash: -3720609337190641477
|
111
|
+
segments:
|
112
|
+
- 0
|
113
|
+
version: "0"
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
115
|
+
none: false
|
116
|
+
requirements:
|
117
|
+
- - ">="
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: "0"
|
120
|
+
requirements: []
|
121
|
+
|
122
|
+
rubyforge_project:
|
123
|
+
rubygems_version: 1.6.1
|
124
|
+
signing_key:
|
125
|
+
specification_version: 3
|
126
|
+
summary: This gem provides an easy solution for connecting to ISBNdb.com's API
|
127
|
+
test_files:
|
128
|
+
- test/helper.rb
|
129
|
+
- test/test_isbndb.rb
|