isbndb 1.5.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
@@ -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
@@ -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
@@ -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.
@@ -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
+ &nbsp;&nbsp;[Terje Tjervaag](https://github.com/terje)<br />
28
+ &nbsp;&nbsp;[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 :)
@@ -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
@@ -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,4 @@
1
+ module ISBNdb
2
+ class AccessKeyError < StandardError; end
3
+ class InvalidURIError < StandardError; end
4
+ 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
@@ -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
@@ -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