├── .gitignore ├── .travis.yml ├── Gemfile ├── README.md ├── Rakefile ├── config └── isbndb.example.yml ├── isbndb.gemspec ├── lib ├── core_extensions │ ├── nil.rb │ └── string.rb ├── isbndb.rb └── isbndb │ ├── access_key_set.rb │ ├── exceptions.rb │ ├── query.rb │ ├── result.rb │ └── result_set.rb └── spec ├── responses ├── access_key_error.xml ├── authors_seth.xml ├── books_hello.xml ├── categories_fiction.xml ├── keystats.xml ├── publishers_francis.xml ├── search.xml ├── single_book.xml └── subjects_ruby.xml ├── spec_helper.rb ├── support └── helpers.rb └── units ├── access_key_set_spec.rb ├── nil_spec.rb ├── query_spec.rb ├── result_set_spec.rb ├── result_spec.rb └── string_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | bin/ 19 | 20 | config/isbndb.yml 21 | *~ 22 | 23 | .rvmrc 24 | .rspec 25 | .ruby-version 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.9.3 3 | - 2.0.0 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Ruby ISBNdb 2 | =========== 3 | 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. 4 | 5 | Version 1.x 6 | ----------- 7 | *ISBNdb 1.x.x has been deprecated!*. You should upgrade to the new version as soon as possible. The old documentation is still available in the [git history](https://github.com/sethvargo/isbndb/tree/75cfe76d096f92b2dfaf1c1b42d7c84ff86fcbc0). There are *significant* changes in this new version, so please test appropriately. 8 | 9 | Installation 10 | ------------ 11 | To get started, install the gem: 12 | 13 | gem install isbndb 14 | 15 | Alternatively, you can add it to your Gemfile: 16 | 17 | ```ruby 18 | gem 'isbndb', '~> 2.0.0' 19 | ``` 20 | 21 | Basic Setup 22 | ----------- 23 | To get started, you'll need to create a `config/isbndb.yml` file in your project root. It should look like this: 24 | 25 | ```yml 26 | access_keys: 27 | - KEY_1 28 | - KEY_2 29 | ... 30 | ``` 31 | 32 | Where you list your access keys. This was in response to security holes in version 1.x where values were passed directly to the initializer. 33 | 34 | Now you're ready to get started: 35 | 36 | ```ruby 37 | @query = ISBNdb::Query.find_book_by_title('Ruby') 38 | ``` 39 | 40 | ActiveRecord-like Usage 41 | ----------------------- 42 | 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: 43 | 44 | ```ruby 45 | ISBNdb::Query.find_book_by_isbn("978-0-9776-1663-3") 46 | ISBNdb::Query.find_books_by_title("Agile Development") 47 | ISBNdb::Query.find_author_by_name("Seth Vargo") 48 | ISBNdb::Query.find_publisher_by_name("Pearson") 49 | ``` 50 | 51 | Advanced Usage 52 | -------------- 53 | Additionally, you can also use a more advanced syntax for complete control: 54 | 55 | ```ruby 56 | ISBNdb::Query.find(:collection => 'books', :where => { :isbn => '978-0-9776-1663-3' }) 57 | ISBNdb::Query.find(:collection => 'books', :where => { :author => 'Seth Vargo' }, :results => 'prices') 58 | ``` 59 | 60 | Options for `:collection` include **books**, **subjects**, **categories**, **authors**, and **publishers**. 61 | 62 | If you are unfamiliar with some of these options, have a look at the [ISBNdb API](http://isbndb.com/docs/api/) 63 | 64 | Processing Results 65 | ------------------ 66 | 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. 67 | 68 | ```ruby 69 | results = ISBNdb::Query.find_books_by_title("Agile Development") 70 | results.each do |result| 71 | puts "title: #{result.title}" 72 | puts "isbn10: #{result.isbn}" 73 | puts "authors: #{result.authors_text}" 74 | end 75 | ``` 76 | 77 | **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: 78 | 79 | ```ruby 80 | puts "title: #{result.title}" unless result.title.nil? 81 | ``` 82 | 83 | versus 84 | 85 | ```ruby 86 | puts "title: #{result.title}" unless result.title.nil? || result.title.blank? || result.title.empty? 87 | ``` 88 | 89 | because ISBNdb.com API is generally inconsistent with respect to returning empty strings, whitespace characters, or nothing at all. 90 | 91 | **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. 92 | 93 | Pagination 94 | ---------- 95 | 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: 96 | 97 | ```ruby 98 | results = ISBNdb::Query.find_books_by_title("ruby") 99 | results.next_page.each do |result| 100 | puts "title: #{result.title}" 101 | end 102 | ``` 103 | 104 | A more realistic example - getting **all** books of a certain title: 105 | 106 | ```ruby 107 | results = ISBNdb::Query.find_books_by_title("ruby") 108 | while results 109 | results.each do |result| 110 | puts "title: #{title}" 111 | end 112 | 113 | results = results.next_page 114 | end 115 | ``` 116 | 117 | It seems incredibly unlikely that a developer would ever use `prev_page`, but it's still there if you need it. 118 | 119 | 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...): 120 | 121 | ```ruby 122 | results = ISBNdb::Query.find_books_by_title("ruby") 123 | results = results.go_to_page(50) # where 50 is the page number you want 124 | ``` 125 | 126 | **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`. 127 | 128 | **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! 129 | 130 | Advanced Key Management 131 | ----------------------- 132 | As over version 2.0, all access key management has moved into the `config/isbndb.yml` file. ISBNdb will auto-rollover if you specify multiple keys. 133 | 134 | Statistics 135 | ---------- 136 | Ruby ISBNdb now supports basic statistics (from the server): 137 | 138 | ```ruby 139 | ISBNdb::Query.keystats # => {:requests => 50, :granted => 49} 140 | ISBNdb::Query.keystats[:granted] # => 49 141 | ``` 142 | 143 | **Note**: Ironically, this information also comes from the server, so it counts as a request... 144 | 145 | Exceptions 146 | ---------- 147 | Ruby ISBNdb could raise the following possible exceptions: 148 | 149 | ```ruby 150 | ISBNdb::AccessKeyError 151 | ``` 152 | 153 | 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. 154 | 155 | A Real-Life Example 156 | ------------------- 157 | 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: 158 | 159 | ```ruby 160 | # books_controller.rb 161 | def simliar 162 | @book = Book.find(params[:id]) 163 | @query = ISBNdb::Query.new(['API-KEY-1', 'API-KEY-2']) 164 | @isbns = @book.similar # returns an array like [1234567890, 0987654321, 3729402827...] 165 | 166 | @isbns.each do |isbn| 167 | begin 168 | (@books ||= []) << ISBNdb::Query.find_book_by_isbn(isbn).first 169 | rescue ISBNdb::AccessKeyError 170 | SomeMailer.send_limit_email.deliver! 171 | end 172 | end 173 | end 174 | ``` 175 | 176 | ```ruby 177 | # similar.html.erb 178 |

The following books are recommeded for you:

179 | <% @books.each do |book| %> 180 |
181 |

<%= book.title_long %>

182 |

authors: <%= book.authors_text %>

183 |
184 | <% end %> 185 | ``` 186 | 187 | Testing 188 | ------- 189 | [![Build Status](http://travis-ci.org/sethvargo/isbndb.png)](http://travis-ci.org/sethvargo/isbndb) 190 | 191 | Change Log 192 | ---------- 193 | 2012-6-17 - Released v2.0 194 | 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 :) 195 | 196 | Acknowledgments 197 | ---------------- 198 | Special thanks to Terje Tjervaag (https://github.com/terje) for giving up the gem name 'isbndb'! 199 | 200 | Special thanks to Lazlo (https://github.com/lazlo) for forwarding his project here! 201 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :setup do 7 | FileUtils.cp('config/isbndb.example.yml', 'config/isbndb.yml') 8 | end 9 | 10 | task default: [:setup, :spec] 11 | -------------------------------------------------------------------------------- /config/isbndb.example.yml: -------------------------------------------------------------------------------- 1 | access_keys: 2 | - ABC123 3 | - DEF456 4 | -------------------------------------------------------------------------------- /isbndb.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | $:.push File.expand_path('../lib', __FILE__) 3 | 4 | Gem::Specification.new do |s| 5 | s.name = 'isbndb' 6 | s.version = '2.0.2' 7 | s.author = 'Seth Vargo' 8 | s.email = 'sethvargo@gmail.com' 9 | s.homepage = 'https://github.com/sethvargo/isbndb' 10 | s.summary = 'Connect with ISBNdb.com\'s API' 11 | s.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 newest version of the gem also features caching, so developers minimize API-key usage.' 12 | 13 | s.files = `git ls-files`.split("\n") 14 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 15 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 16 | s.require_paths = ["lib"] 17 | 18 | # Runtime dependencies 19 | s.add_runtime_dependency 'httparty', '~> 0.12' 20 | s.add_runtime_dependency 'rake' 21 | 22 | # Development dependencies 23 | s.add_development_dependency 'rspec', '~> 2.14' 24 | s.add_development_dependency 'webmock', '~> 1.15' 25 | end 26 | -------------------------------------------------------------------------------- /lib/core_extensions/nil.rb: -------------------------------------------------------------------------------- 1 | class NilClass 2 | def blank? 3 | true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/core_extensions/string.rb: -------------------------------------------------------------------------------- 1 | class String 2 | require 'date' 3 | 4 | def is_plural? 5 | self.downcase.pluralize == self.downcase 6 | end 7 | 8 | def is_singular? 9 | !self.is_plural? 10 | end 11 | 12 | def titleize 13 | str = self[0].upcase + self[1..-1].downcase 14 | end 15 | 16 | def singularize 17 | str = self.dup 18 | if str[-3..-1] == 'ies' 19 | str[0..-4] + 'y' 20 | elsif str[-1] == 's' 21 | str[0..-2] 22 | else 23 | str 24 | end 25 | end 26 | 27 | def pluralize 28 | str = self.dup 29 | if str[-1] == 'y' 30 | str[0..-2] + 'ies' 31 | elsif str[-1] == 's' 32 | str 33 | else 34 | str + 's' 35 | end 36 | end 37 | 38 | def blank? 39 | dup.strip.length == 0 ? true : false 40 | end 41 | 42 | def underscore 43 | self.dup.gsub(/::/, '/'). 44 | gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). 45 | gsub(/([a-z\d])([A-Z])/,'\1_\2'). 46 | tr("-", "_"). 47 | downcase 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/isbndb.rb: -------------------------------------------------------------------------------- 1 | module ISBNdb 2 | require 'httparty' 3 | 4 | require 'isbndb/access_key_set' 5 | require 'isbndb/exceptions' 6 | require 'isbndb/query' 7 | require 'isbndb/result_set' 8 | require 'isbndb/result' 9 | 10 | require 'core_extensions/string' 11 | require 'core_extensions/nil' 12 | end 13 | -------------------------------------------------------------------------------- /lib/isbndb/access_key_set.rb: -------------------------------------------------------------------------------- 1 | module ISBNdb 2 | protected 3 | # The AccessKeySet is a simple class used to manage access keys. It is used primarily 4 | # by the ruby_isbndb class to automatically advance access keys when necessary. 5 | class AccessKeySet 6 | # Create the @access_keys array and then verify that the keys are valid keys. 7 | def initialize 8 | @access_keys ||= YAML::load(File.open('config/isbndb.yml'))['access_keys'] 9 | end 10 | 11 | # Returns the total number of access keys in this set. 12 | def size 13 | @access_keys.size 14 | end 15 | 16 | def current_index 17 | @current_index ||= 0 18 | end 19 | 20 | # Get the current key. It returns a string of the access key. 21 | def current_key 22 | @access_keys[current_index] 23 | end 24 | 25 | # Move the key pointer forward. 26 | def next_key! 27 | @current_index = current_index + 1 28 | current_key 29 | end 30 | 31 | # Get the next key. 32 | def next_key 33 | @access_keys[current_index+1] 34 | end 35 | 36 | # Move the key pointer back. 37 | def prev_key! 38 | @current_index = current_index - 1 39 | current_key 40 | end 41 | 42 | # Get the previous key. 43 | def prev_key 44 | @access_keys[current_index-1] 45 | end 46 | 47 | # Tell Ruby ISBNdb to use a specified key. If the key does not exist, it is 48 | # added to the set and set as the current key. 49 | def use_key(key) 50 | @current_index = @access_keys.index(key) || @access_keys.push(key).index(key) 51 | current_key 52 | end 53 | 54 | # Remove the given access key from the AccessKeySet. 55 | def remove_key(key) 56 | @access_keys.delete(key) 57 | end 58 | 59 | # Pretty print the AccessKeySet 60 | def to_s 61 | "#" 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/isbndb/exceptions.rb: -------------------------------------------------------------------------------- 1 | module ISBNdb 2 | class AccessKeyError < StandardError; end 3 | end 4 | -------------------------------------------------------------------------------- /lib/isbndb/query.rb: -------------------------------------------------------------------------------- 1 | module ISBNdb 2 | # The Query object is the most important class of the ISBNdb Module. It is the only public 3 | # class, and handles the processing power. 4 | class Query 5 | include HTTParty 6 | base_uri 'http://isbndb.com/api' 7 | headers 'Content-Type' => 'text/xml' 8 | 9 | # This is the generic find method. It accepts a hash of parameters including :collection, 10 | # :where clauses, and :results to show. It builds the corresponding URI and sends that URI 11 | # off to the ResultSet for processing. 12 | def self.find(params = {}) 13 | raise 'No parameters specified! You must specify at least one parameter!' unless params[:where] 14 | raise 'params[:where] cannot be a String! It must be a Hash!' if params[:where].is_a?(String) 15 | raise 'params[:where] cannot be an Array! It must be a Hash!' if params[:where].is_a?(Array) 16 | 17 | collection = params[:collection] ||= :books 18 | results = params[:results] ||= :details 19 | results = [results].flatten 20 | 21 | # build the search clause 22 | searches = [] 23 | params[:where].each_with_index do |(key,val), i| 24 | searches << "index#{i+1}=#{key.to_s.strip}" 25 | searches << "value#{i+1}=#{val.to_s.strip}" 26 | end 27 | 28 | # make the request 29 | uri = "/#{collection}.xml?access_key=#{access_key_set.current_key}&results=#{results.join(',')}&#{searches.join('&')}" 30 | ISBNdb::ResultSet.new(uri, collection) 31 | rescue ISBNdb::AccessKeyError 32 | access_key_set.next_key! 33 | retry unless access_key_set.current_key.nil? 34 | raise 35 | end 36 | 37 | # This method returns keystats about your API key, including the number of requests 38 | # and the number of granted requets. Be advised that this request actually counts 39 | # as a request to the server, so use with caution. 40 | def self.keystats 41 | result = self.get("/books.xml?access_key=#{access_key_set.current_key}&results=keystats") 42 | result.parsed_response['ISBNdb']['KeyStats'] || {} 43 | end 44 | 45 | # Method missing allows for dynamic finders, similar to that of ActiveRecord. See 46 | # the README for more information on using magic finders. 47 | def self.method_missing(m, *args, &block) 48 | method = m.to_s.downcase 49 | 50 | if method.match(/find_(.+)_by_(.+)/) 51 | split = method.split('_', 4) 52 | collection, search_strs = split[1].downcase.pluralize, [split.last] 53 | 54 | # check and see if we are searching multiple fields 55 | search_strs = search_strs.first.split('_and_') if(search_strs.first.match(/_and_/)) 56 | raise "Wrong Number of Arguments (#{args.size} for #{search_strs.size})" if args.size != search_strs.size 57 | 58 | # create the searches hash 59 | searches = {} 60 | search_strs.each_with_index { |str, i| searches[str.strip.to_sym] = args[i].strip } 61 | 62 | return find(:collection => collection, :where => searches) 63 | end 64 | 65 | super 66 | end 67 | 68 | # Pretty print the Query object with the access key. 69 | def self.to_s 70 | "#" 71 | end 72 | 73 | private 74 | def self.access_key_set 75 | @@access_key_set ||= ISBNdb::AccessKeySet.new 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/isbndb/result.rb: -------------------------------------------------------------------------------- 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(json = {}) 13 | @store = build_result(json) 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 myriad of exceptions throughout their code. 19 | def method_missing(m, *args, &block) 20 | @store[m.to_s.underscore] 21 | end 22 | 23 | # Return a list of all "methods" this class responds to 24 | def instance_methods 25 | @store.collect{ |key,value| key.to_s } 26 | end 27 | 28 | # Pretty print the Result including the number of singleton methods that exist. If 29 | # you want the ACTUAL singleton methods, call @result.singleton_methods. 30 | def to_s 31 | "#" 32 | end 33 | 34 | def inspect 35 | "# ' + value.inspect }.join(', ')}>" 36 | end 37 | 38 | def ==(result) 39 | self.inspect == result.inspect 40 | end 41 | 42 | private 43 | def build_result(json) 44 | result = {} 45 | 46 | json.each do |key,value| 47 | result[key.to_s.underscore] = if value.is_a?(Hash) 48 | build_result(value) 49 | elsif value.blank? 50 | nil 51 | else 52 | value 53 | end 54 | end 55 | 56 | result 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/isbndb/result_set.rb: -------------------------------------------------------------------------------- 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 | include HTTParty 10 | 11 | base_uri 'http://isbndb.com/api' 12 | headers 'Content-Type' => 'text/xml' 13 | 14 | # This method creates instance variables for @uri, @collection, and @current_page. It then 15 | # attempts to parse the XML at the gieven URI. If it cannot parse the URI for any reason, 16 | # it will raise an ISBNdb::InvalidURIError. Next, the results are checked for an any error 17 | # messages. An ISBNdb::AccessKeyError will be raised if the results contain any errors. 18 | # Finally, this method then actually builds the ResultSet. 19 | def initialize(uri, collection, current_page = 1) 20 | @uri = URI.escape(uri) 21 | @collection = collection.to_s.titleize.singularize 22 | @current_page = current_page 23 | @parsed_response = self.class.get(@uri).parsed_response['ISBNdb'] 24 | 25 | check_results 26 | build_results 27 | end 28 | 29 | def size 30 | @results.size 31 | end 32 | 33 | # Because ResultSet extends Enumerable, we need to define the each method. This allows users 34 | # to call methods like .first, .last, and .each on the ResultSet, making it behave like 35 | # a primitive array. 36 | def each(&block) 37 | @results.each &block 38 | end 39 | 40 | # Access via index 41 | def [](i) 42 | @results[i] 43 | end 44 | 45 | # Jump to a specific page. This method will return nil if the specified page does not exist. 46 | def go_to_page(page) 47 | get_total_pages unless @total_pages 48 | return nil if page.to_i < 1 || page.to_i > @total_pages 49 | ISBNdb::ResultSet.new("#{@uri}&page_number=#{page}", @collection, page) 50 | end 51 | 52 | # Go to the next page. This method will return nil if a next page does not exist. 53 | def next_page 54 | go_to_page(@current_page+1) 55 | end 56 | 57 | # Go to the previous page. This method will return nil if a previous page does not exist. 58 | def prev_page 59 | go_to_page(@current_page-1) 60 | end 61 | 62 | # Pretty prints the Result set information. 63 | def to_s 64 | "# #{@results.size}>" 65 | end 66 | 67 | def ==(result_set) 68 | self.size == result_set.size && self.instance_variable_get('@results') == result_set.instance_variable_get('@results') 69 | end 70 | 71 | private 72 | # Check the results for an error message. If one exists, raise an ISBNdb::AccessKeyError for now. 73 | # Currently the API does not differentiate between an overloaded API key and an invalid one 74 | # (it returns the same exact response), so there only exists one exception for now... 75 | def check_results 76 | raise ISBNdb::AccessKeyError if @parsed_response['ErrorMessage'] 77 | end 78 | 79 | # Iterate over #{@collection}List/#{@collection}Data (ex. BookList/BookData) and build a result with 80 | # each child. This method works because the API always returns #{@collection}List followed by a subset 81 | # of #{@collection}Data. These results are all pushed into the @results array for accessing. 82 | def build_results 83 | result_json = @parsed_response["#{@collection}List"]["#{@collection}Data"] 84 | if result_json.is_a?(Hash) ## One result, typically from find_by_isbn 85 | @results = [Result.new(result_json)] 86 | else 87 | @results = (result_json || []).collect{ |json| Result.new(json) } 88 | end 89 | end 90 | 91 | # This helper method is mainly designed for use with the go_to_page(page) method. It parses the XML 92 | # and returns the total number of pages that exist for this result set. 93 | def get_total_pages 94 | list = @parsed_response["#{@collection}List"] 95 | @total_pages = (list['total_results'].to_f/list['page_size'].to_f).ceil 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /spec/responses/access_key_error.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Access key error 5 | 6 | -------------------------------------------------------------------------------- /spec/responses/authors_seth.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Abraham, Seth 7 |
8 | 9 | 10 | Abramson, Seth F. 11 |
12 | 13 | 14 | Adams, Seth 15 |
16 | 17 | 18 | Seth Afikorah-Danquah 19 |
20 | 21 | 22 | Agata, Seth H 23 |
24 | 25 | 26 | Agbo, Seth A. 27 |
28 | 29 | 30 | Allcorn, Seth 31 |
32 | 33 | 34 | Ames, Seth 35 |
36 | 37 | 38 | Anderson, Seth B. 39 |
40 | 41 | 42 | Seth Andrew 43 |
44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /spec/responses/books_hello.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 100th Day of School 7 | 100th Day of School (Hello Reader Level 2) 8 | Angela Shelf Medearis, 9 | Fitzgerald Books 10 |
11 | 12 | 13 | 100th Day, The 14 | 100th Day, The (level 1) (Hello Reader Level 1) 15 | Alayne Pick, Grace Maccarone, Laura Freeman (Illustrator) 16 | Cartwheel 17 |
18 | 19 | 20 | 2011 Hello Kitty Engagement Calendar 21 | 22 | Day Dream (Contributor) 23 | Day Dream 24 |
25 | 26 | 27 | 2011 Hello Kitty Wall Calendar 28 | 29 | Day Dream (Contributor) 30 | Day Dream 31 |
32 | 33 | 34 | 2012 Hello Kitty 2 Year Pocket Planner Calendar 35 | 36 | Day Dream, 37 | Day Dream 38 |
39 | 40 | 41 | 2012 Hello Kitty Juvenile Activity Calendar 42 | 43 | Day Dream, 44 | Day Dream 45 |
46 | 47 | 48 | 2012 Hello Kitty Mini Calendar 49 | 50 | Day Dream, 51 | Day Dream 52 |
53 | 54 | 55 | 2012 Hello Kitty Wall Calendar 56 | 57 | Day Dream, 58 | Day Dream 59 |
60 | 61 | 62 | 2012 Hello Kitty Weekly Engagement Calendar 63 | 64 | Day Dream, 65 | Day Dream 66 |
67 | 68 | 69 | Hello gorgeous 70 | Hello gorgeous: a guide to style 71 | by Kristen Kemp 72 | New York : Scholastic, c2000. 73 |
74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /spec/responses/categories_fiction.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Fiction 7 |
8 | 9 | 10 | Fiction 11 |
12 | 13 | 14 | Fiction 15 |
16 | 17 | 18 | Fiction 19 |
20 | 21 | 22 | Fiction 23 |
24 | 25 | 26 | Fiction 27 |
28 | 29 | 30 | Fiction 31 |
32 | 33 | 34 | Fiction 35 |
36 | 37 | 38 | Fiction 39 |
40 | 41 | 42 | Fiction 43 |
44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /spec/responses/keystats.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /spec/responses/publishers_francis.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | : Taylor & Francis 7 |
8 | 9 | 10 | A.A. Balkema/Taylor & Francis 11 |
12 | 13 | 14 | Albert Francis Perrin 15 |
16 | 17 | 18 | Auerbach Publications, Taylor & Francis Group 19 |
20 | 21 | 22 | Auerbach Publications/Taylor & Francis 23 |
24 | 25 | 26 | Auerbach Publications/Taylor & Francis Group 27 |
28 | 29 | 30 | Auerbach/Taylor&Francis 31 |
32 | 33 | 34 | B. Francis 35 |
36 | 37 | 38 | Balkema/Taylor & Francis 39 |
40 | 41 | 42 | Beau Francis Press 43 |
44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /spec/responses/search.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 1000s of Amazing Uses for Everyday Products 7 | 8 | Betsy Rossen Elliot, 9 | Publications International, Ltd. 10 |
11 | 12 | 13 | 1000 more amazing facts for kids 14 | 15 | Keith Smithcartoons by David Mostyn 16 | London Ward Lock c1986 17 |
18 | 19 | 20 | 1001 Amazing Animal Facts 21 | 22 | Marc Powell, 23 | Arcturus Publishing Ltd 24 |
25 | 26 | 27 | 1001 Amazing Facts 28 | 29 | 30 | Egmont Childrens Books 31 |
32 | 33 | 34 | 1001 Stupid Sports Quotes 35 | 1001 Stupid Sports Quotes: Jaw-Dropping, Stupefying, and Amazing Expressions from the World's Best Athletes 36 | Randy Howe (Editor) 37 | The Lyons Press 38 |
39 | 40 | 41 | 100 Amazing Americans 42 | 43 | 44 | The Trumpet Club 45 |
46 | 47 | 48 | 100 amazing answers to prayer 49 | 50 | William J. Petersen and Randy Petersen 51 | Grand Rapids, MI : F.H. Revell, c2003. 52 |
53 | 54 | 55 | 100 Amazing Answers to Prayer 56 | 57 | William J. Petersen, Randy Petersen, 58 | Revell 59 |
60 | 61 | 62 | 100 Amazing Award-Winning Science Fair Projects 63 | 64 | Glen Vecchione, 65 | Sterling 66 |
67 | 68 | 69 | 100 Amazing Crosswords 70 | 71 | Thomas Joseph, 72 | Puzzlewright 73 |
74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /spec/responses/single_book.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 100th Day of School 7 | 100th Day of School (Hello Reader Level 2) 8 | Angela Shelf Medearis, 9 | Fitzgerald Books 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /spec/responses/subjects_ruby.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ruby akademicki 7 | 8 | 9 | Bell, Ruby 10 | 11 | 12 | Bell, Ruby,1942 13 | 14 | 15 | Braff, Ruby,1927-2003 16 | 17 | 18 | Bridges, Ruby 19 | 20 | 21 | Bridges, Ruby -- Comic books, strips, etc 22 | 23 | 24 | Crane, Ruby (Fictitious character) -- Fiction 25 | 26 | 27 | Cress, Ruby,1911-Correspondence 28 | 29 | 30 | Dee, Ruby 31 | 32 | 33 | Duncan, Ruby 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'webmock/rspec' 3 | 4 | Dir[File.expand_path('spec/support/**/*.rb')].each { |f| require f } 5 | 6 | require 'isbndb' 7 | 8 | RSpec.configure do |config| 9 | config.include Helpers 10 | end 11 | -------------------------------------------------------------------------------- /spec/support/helpers.rb: -------------------------------------------------------------------------------- 1 | module Helpers 2 | # Stub YAML::load to return the hash of keys that we specify 3 | def stub_access_keys(*keys) 4 | YAML.stub(:load) do 5 | { 'access_keys' => keys } 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/units/access_key_set_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ISBNdb::AccessKeySet do 4 | before do 5 | stub_access_keys('ABC123', 'DEF456', 'GHI789') 6 | @access_key_set = ISBNdb::AccessKeySet.new 7 | end 8 | 9 | context 'size' do 10 | it 'should return 0 if there are no keys' do 11 | stub_access_keys 12 | ISBNdb::AccessKeySet.new.size.should == 0 13 | end 14 | 15 | it 'should return the number of access keys' do 16 | @access_key_set.size.should == 3 17 | end 18 | end 19 | 20 | context 'current_index' do 21 | it 'should return the current index' do 22 | @access_key_set.current_index.should == 0 23 | end 24 | end 25 | 26 | context 'current_key' do 27 | it 'should return the current key' do 28 | @access_key_set.current_key.should == 'ABC123' 29 | end 30 | end 31 | 32 | context 'next_key!' do 33 | it 'should advance to the next key' do 34 | expect{ @access_key_set.next_key! }.to change{ @access_key_set.current_key }.from('ABC123').to('DEF456') 35 | end 36 | 37 | it 'should return the new key' do 38 | @access_key_set.next_key!.should == 'DEF456' 39 | end 40 | end 41 | 42 | context 'next_key' do 43 | it 'should return the next key' do 44 | @access_key_set.next_key.should == 'DEF456' 45 | end 46 | end 47 | 48 | context 'prev_key!' do 49 | before do 50 | @access_key_set.instance_variable_set('@current_index', 1) 51 | end 52 | 53 | it 'should de-advance to the prev key' do 54 | expect{ @access_key_set.prev_key! }.to change{ @access_key_set.current_key }.from('DEF456').to('ABC123') 55 | end 56 | 57 | it 'should return the new key' do 58 | @access_key_set.prev_key!.should == 'ABC123' 59 | end 60 | end 61 | 62 | context 'prev_key' do 63 | before do 64 | @access_key_set.instance_variable_set('@current_index', 1) 65 | end 66 | 67 | it 'should return the prev key' do 68 | @access_key_set.prev_key.should == 'ABC123' 69 | end 70 | end 71 | 72 | context 'use_key' do 73 | it 'should use an existing key' do 74 | @access_key_set.use_key('GHI789').should == 'GHI789' 75 | end 76 | 77 | it 'should create a new key if it does not already exist' do 78 | @access_key_set.use_key('NEW_KEY').should == 'NEW_KEY' 79 | @access_key_set.instance_variable_get('@access_keys').should include('NEW_KEY') 80 | end 81 | end 82 | 83 | context 'remove_key' do 84 | it 'should do nothing if the key does not exist' do 85 | expect{ @access_key_set.remove_key('NOPE') }.not_to change{ @access_key_set.instance_variable_get('@access_keys') } 86 | end 87 | 88 | it 'should remove the key if it exists' do 89 | expect{ @access_key_set.remove_key('ABC123') }.to change{ @access_key_set.instance_variable_get('@access_keys') }.from(['ABC123', 'DEF456', 'GHI789']).to(['DEF456', 'GHI789']) 90 | end 91 | end 92 | 93 | context 'to_s' do 94 | it 'should return the correct string' do 95 | @access_key_set.to_s.should == '#' 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /spec/units/nil_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe NilClass do 4 | context 'blank?' do 5 | it 'should return true' do 6 | nil.blank?.should be_true 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/units/query_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ISBNdb::Query do 4 | before do 5 | stub_access_keys('ABC123') 6 | end 7 | 8 | context 'find' do 9 | context 'without params[:where]' do 10 | it 'should throw an exception' do 11 | lambda{ ISBNdb::Query.find }.should raise_error, 'No parameters specified! You must specify at least one parameter!' 12 | end 13 | end 14 | 15 | context 'with params[:where]' do 16 | context 'as a string' do 17 | it 'should throw an exception' do 18 | lambda{ ISBNdb::Query.find(:where => 'anything') }.should raise_error, 'params[:where] cannot be a String! It must be a Hash!' 19 | end 20 | end 21 | 22 | context 'as an array' do 23 | it 'should throw an exception' do 24 | lambda{ ISBNdb::Query.find(:where => ['anything', 'else']) }.should raise_error, 'params[:where] cannot be an Array! It must be a Hash!' 25 | end 26 | end 27 | 28 | context 'as a hash' do 29 | before do 30 | stub_request(:get, "http://isbndb.com/api/books.xml?access_key=ABC123&index1=title&results=details&value1=amazing"). 31 | to_return(:body => File.new('spec/responses/search.xml'), :headers => {'Content-Type'=>'text/xml'}) 32 | end 33 | 34 | it 'should not throw an exception' do 35 | lambda{ ISBNdb::Query.find(:where => {:title => 'amazing'} ) }.should_not raise_error 36 | end 37 | 38 | it 'should return an ISBNdb::ResultSet' do 39 | ISBNdb::Query.find(:where => {:title => 'amazing'}).should be_a(ISBNdb::ResultSet) 40 | end 41 | end 42 | 43 | context 'with an access key error' do 44 | before do 45 | stub_request(:get, "http://isbndb.com/api/books.xml?access_key=ABC123&index1=title&results=details&value1=amazing").to_return(:body => File.new('spec/responses/access_key_error.xml'), :headers => {'Content-Type'=>'text/xml'}) 46 | end 47 | 48 | it 'should raise an exception' do 49 | lambda{ ISBNdb::Query.find(:where => {:title => 'amazing'}) }.should raise_error ISBNdb::AccessKeyError 50 | end 51 | 52 | after do 53 | ISBNdb::Query.access_key_set.prev_key! 54 | end 55 | end 56 | end 57 | end 58 | 59 | context 'keystats' do 60 | before do 61 | stub_request(:get, 'http://isbndb.com/api/books.xml?access_key=ABC123&results=keystats').to_return(:body => File.new('spec/responses/keystats.xml'), :headers => { 'Content-Type' => 'text/xml' }) 62 | end 63 | 64 | it 'should return an Hash' do 65 | ISBNdb::Query.keystats.should be_a(Hash) 66 | end 67 | 68 | it('should return the number of granted requests'){ ISBNdb::Query.keystats['granted'].should == '2' } 69 | it('should return the access key'){ ISBNdb::Query.keystats['access_key'].should == 'ABC123' } 70 | it('should return the number of requests made'){ ISBNdb::Query.keystats['requests'].should == '5' } 71 | it('should return the account limit'){ ISBNdb::Query.keystats['limit'].should == '0' } 72 | end 73 | 74 | context 'to_s' do 75 | it 'the properly formatted string' do 76 | ISBNdb::Query.to_s.should =='#' 77 | end 78 | end 79 | 80 | context 'method_missing' do 81 | context 'for a valid method it can handle' do 82 | it 'should work for books' do 83 | stub_request(:get, "http://isbndb.com/api/books.xml?access_key=ABC123&index1=title&results=details&value1=hello").to_return(:body => File.new('spec/responses/books_hello.xml'), :headers => {'Content-Type'=>'text/xml'}) 84 | 85 | lambda{ ISBNdb::Query.find_book_by_title('hello') }.should_not raise_error 86 | lambda{ ISBNdb::Query.find_books_by_title('hello') }.should_not raise_error 87 | 88 | @books = ISBNdb::Query.find_books_by_title('hello') 89 | @books.size.should == 10 90 | 91 | @book = @books.first 92 | @book.book_id.should == '100th_day_of_school_a04' 93 | @book.isbn.should == '1590543947' 94 | @book.isbn13.should == '9781590543948' 95 | @book.title.should == '100th Day of School' 96 | @book.title_long.should == '100th Day of School (Hello Reader Level 2)' 97 | @book.authors_text.should == 'Angela Shelf Medearis, ' 98 | @book.publisher_text.should be_a(Hash) 99 | @book.details.should be_a(Hash) 100 | end 101 | 102 | it 'should work for subjects' do 103 | stub_request(:get, "http://isbndb.com/api/subjects.xml?access_key=ABC123&index1=name&results=details&value1=Ruby").to_return(:body => File.new('spec/responses/subjects_ruby.xml'), :headers => {'Content-Type'=>'text/xml'}) 104 | 105 | lambda{ ISBNdb::Query.find_subject_by_name('Ruby') }.should_not raise_error 106 | lambda{ ISBNdb::Query.find_subjects_by_name('Ruby') }.should_not raise_error 107 | 108 | @subjects = ISBNdb::Query.find_subjects_by_name('Ruby') 109 | @subjects.size.should == 10 110 | 111 | @subject = @subjects.first 112 | @subject.subject_id.should == 'ruby_napdowe_eksploatacja_podrcznik_akademicki' 113 | @subject.book_count.should == '1' 114 | @subject.marc_field.should == '650' 115 | @subject.marc_indicator_1.should be_nil 116 | @subject.marc_indicator_2.should == '9' 117 | @subject.name.should == 'ruby akademicki' 118 | end 119 | 120 | it 'should work for categories' do 121 | stub_request(:get, "http://isbndb.com/api/categories.xml?access_key=ABC123&index1=name&results=details&value1=fiction").to_return(:body => File.new('spec/responses/categories_fiction.xml'), :headers => {'Content-Type'=>'text/xml'}) 122 | 123 | lambda{ ISBNdb::Query.find_category_by_name('fiction') }.should_not raise_error 124 | lambda{ ISBNdb::Query.find_categories_by_name('fiction') }.should_not raise_error 125 | 126 | @categories = ISBNdb::Query.find_categories_by_name('fiction') 127 | @categories.size.should == 10 128 | 129 | @category = @categories.first 130 | @category.category_id.should == 'society.religion.christianity.denominations.catholicism.literature.fiction' 131 | @category.parent_id.should == 'society.religion.christianity.denominations.catholicism.literature' 132 | @category.name.should == 'Fiction' 133 | @category.details.should be_a(Hash) 134 | end 135 | 136 | it 'should work for authors' do 137 | stub_request(:get, "http://isbndb.com/api/authors.xml?access_key=ABC123&index1=name&results=details&value1=Seth").to_return(:body => File.new('spec/responses/authors_seth.xml'), :headers => {'Content-Type'=>'text/xml'}) 138 | 139 | lambda{ ISBNdb::Query.find_author_by_name('Seth') }.should_not raise_error 140 | lambda{ ISBNdb::Query.find_authors_by_name('Seth') }.should_not raise_error 141 | 142 | @authors = ISBNdb::Query.find_authors_by_name('Seth') 143 | @authors.size.should == 10 144 | 145 | @author = @authors.first 146 | @author.person_id.should == 'abraham_seth' 147 | @author.name.should == 'Abraham, Seth' 148 | @author.details.should be_a(Hash) 149 | end 150 | 151 | it 'should work for publishers' do 152 | stub_request(:get, "http://isbndb.com/api/publishers.xml?access_key=ABC123&index1=name&results=details&value1=Francis").to_return(:body => File.new('spec/responses/publishers_francis.xml'), :headers => {'Content-Type'=>'text/xml'}) 153 | 154 | lambda{ ISBNdb::Query.find_publisher_by_name('Francis') }.should_not raise_error 155 | lambda{ ISBNdb::Query.find_publishers_by_name('Francis') }.should_not raise_error 156 | 157 | @publishers = ISBNdb::Query.find_publishers_by_name('Francis') 158 | @publishers.size.should == 10 159 | 160 | @publisher = @publishers.first 161 | @publisher.publisher_id.should == 'taylor_francis_a01' 162 | @publisher.name.should == ': Taylor & Francis' 163 | @publisher.details.should be_a(Hash) 164 | end 165 | end 166 | 167 | context 'for an invalid method it can handle' do 168 | it 'should throw an exception' do 169 | lambda{ ISBNdb::Query.find_foo_by_bar('totes') }.should raise_error 170 | end 171 | end 172 | 173 | context 'for a method is can\'t handle' do 174 | it 'should throw an exception' do 175 | lambda{ ISBNdb::Query.foo_bar }.should raise_error 176 | end 177 | end 178 | end 179 | end 180 | -------------------------------------------------------------------------------- /spec/units/result_set_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # These examples are used to test multiple books returned and a single book return 4 | # Actual specs at the bottom of this file 5 | 6 | shared_examples "AProperResultSet" do 7 | context 'initialize' do 8 | it 'should set all the instance variables' do 9 | @result_set.instance_variable_get('@uri').should == '/books.xml?access_key=ABC123&index1=title&results=details&value1=hello' 10 | @result_set.instance_variable_get('@collection').should == 'Book' 11 | @result_set.instance_variable_get('@current_page').should == 1 12 | @result_set.instance_variable_get('@parsed_response').should == @expected_parsed_response 13 | end 14 | end 15 | 16 | context 'size' do 17 | it 'should return the size of @results' do 18 | @result_set.size.should == @expected_result_size 19 | end 20 | end 21 | 22 | context 'each' do 23 | it 'should iterate over @results' do 24 | @result_set.each_with_index do |result,index| 25 | result.should == @result_set.instance_variable_get('@results')[index] 26 | end 27 | end 28 | end 29 | 30 | context '[]' do 31 | it 'should return the first result' do 32 | @result_set[0].should == @result_set.first 33 | end 34 | end 35 | 36 | context 'go_to_page' do 37 | it 'should get the total number of pages' do 38 | expect{ @result_set.go_to_page(1) }.to change { @result_set.instance_variable_get('@total_pages') }.to(167) 39 | end 40 | 41 | it 'should return if the page is invalid' do 42 | @result_set.go_to_page('foo').should be_nil 43 | @result_set.go_to_page(-1).should be_nil 44 | @result_set.go_to_page(138193289).should be_nil 45 | end 46 | 47 | it 'should return a new result set' do 48 | @result_set.go_to_page(2).should be_a(ISBNdb::ResultSet) 49 | @result_set.go_to_page(2).instance_variable_get('@uri').should include('page_number=2') 50 | end 51 | end 52 | 53 | context 'next_page' do 54 | it 'should go to the next page' do 55 | @result_set.next_page.should == @result_set.go_to_page(2) 56 | end 57 | 58 | it 'should return nil when there are no more pages' do 59 | @result_set.go_to_page(167).next_page.should be_nil 60 | end 61 | end 62 | 63 | context 'prev_page' do 64 | it 'should go to the previous page' do 65 | @result_set.go_to_page(2).prev_page.should == @result_set.go_to_page(1) 66 | end 67 | 68 | it 'should return nil when there is no previous page' do 69 | @result_set.go_to_page(1).prev_page.should be_nil 70 | end 71 | end 72 | 73 | context 'to_s' do 74 | it 'should return the correct string' do 75 | @result_set.to_s.should == '# ' + @expected_result_size.to_s + '>' 76 | end 77 | end 78 | 79 | context 'check_results' do 80 | it 'should raise an exception if there is an error message' do 81 | stub_request(:get, /http:\/\/isbndb\.com\/api\/books\.xml\?access_key=ABC123&index1=title(&page_number=(.+))?&results=details&value1=hello/).to_return(:body => File.new('spec/responses/access_key_error.xml'), :headers => {'Content-Type'=>'text/xml'}) 82 | proc = lambda{ ISBNdb::ResultSet.new('/books.xml?access_key=ABC123&index1=title&results=details&value1=hello', :books) } 83 | 84 | proc.should raise_error(ISBNdb::AccessKeyError) 85 | end 86 | end 87 | 88 | context 'get_total_pages' do 89 | it 'should get the total number of pages' do 90 | @result_set.send(:get_total_pages).should == 167 91 | end 92 | end 93 | end 94 | 95 | describe "MultipleISBNdb::ResultSet" do 96 | before do 97 | stub_request(:get, /http:\/\/isbndb\.com\/api\/books\.xml\?access_key=ABC123&index1=title(&page_number=(.+))?&results=details&value1=hello/).to_return(:body => File.new('spec/responses/books_hello.xml'), :headers => {'Content-Type'=>'text/xml'}) 98 | @result_set = ISBNdb::ResultSet.new('/books.xml?access_key=ABC123&index1=title&results=details&value1=hello', :books) 99 | @expected_result_size = 10 100 | @expected_parsed_response = {"server_time"=>"2012-06-16T20:10:13Z", "BookList"=>{"total_results"=>"1664", "page_size"=>"10", "page_number"=>"1", "shown_results"=>"10", "BookData"=>[{"book_id"=>"100th_day_of_school_a04", "isbn"=>"1590543947", "isbn13"=>"9781590543948", "Title"=>"100th Day of School", "TitleLong"=>"100th Day of School (Hello Reader Level 2)", "AuthorsText"=>"Angela Shelf Medearis, ", "PublisherText"=>{"publisher_id"=>"fitzgerald_books", "__content__"=>"Fitzgerald Books"}, "Details"=>{"change_time"=>"2011-03-08T18:28:45Z", "price_time"=>"2012-05-29T16:45:46Z", "edition_info"=>"Unknown Binding; 2007-01", "language"=>"", "physical_description_text"=>"6.0\"x9.0\"x0.5\"; 0.4 lb; 32 pages", "lcc_number"=>"", "dewey_decimal_normalized"=>"", "dewey_decimal"=>""}}, {"book_id"=>"100th_day_the", "isbn"=>"0439330173", "isbn13"=>"9780439330176", "Title"=>"100th Day, The", "TitleLong"=>"100th Day, The (level 1) (Hello Reader Level 1)", "AuthorsText"=>"Alayne Pick, Grace Maccarone, Laura Freeman (Illustrator)", "PublisherText"=>{"publisher_id"=>"cartwheel", "__content__"=>"Cartwheel"}, "Details"=>{"change_time"=>"2006-02-27T21:34:15Z", "price_time"=>"2012-05-29T16:46:08Z", "edition_info"=>"Paperback; 2002-12-01", "language"=>"", "physical_description_text"=>"32 pages", "lcc_number"=>"", "dewey_decimal_normalized"=>"", "dewey_decimal"=>""}}, {"book_id"=>"2011_hello_kitty_engagement_calendar", "isbn"=>"1423803558", "isbn13"=>"9781423803553", "Title"=>"2011 Hello Kitty Engagement Calendar", "TitleLong"=>nil, "AuthorsText"=>"Day Dream (Contributor)", "PublisherText"=>{"publisher_id"=>"day_dream", "__content__"=>"Day Dream"}, "Details"=>{"change_time"=>"2010-09-21T21:20:39Z", "price_time"=>"2012-03-24T04:44:44Z", "edition_info"=>"Calendar; 2010-08-01", "language"=>"", "physical_description_text"=>"7.0\"x8.5\"x0.9\"; 1.1 lb", "lcc_number"=>"", "dewey_decimal_normalized"=>"741", "dewey_decimal"=>"741"}}, {"book_id"=>"2011_hello_kitty_wall_calendar", "isbn"=>"1423803981", "isbn13"=>"9781423803980", "Title"=>"2011 Hello Kitty Wall Calendar", "TitleLong"=>nil, "AuthorsText"=>"Day Dream (Contributor)", "PublisherText"=>{"publisher_id"=>"day_dream", "__content__"=>"Day Dream"}, "Details"=>{"change_time"=>"2010-09-21T18:46:02Z", "price_time"=>"2012-04-25T20:26:40Z", "edition_info"=>"Calendar; 2010-08-01", "language"=>"", "physical_description_text"=>"10.8\"x11.8\"x0.2\"; 0.3 lb", "lcc_number"=>"", "dewey_decimal_normalized"=>"", "dewey_decimal"=>""}}, {"book_id"=>"2012_hello_kitty_2_year_pocket_planner_calendar", "isbn"=>"1423809424", "isbn13"=>"9781423809425", "Title"=>"2012 Hello Kitty 2 Year Pocket Planner Calendar", "TitleLong"=>nil, "AuthorsText"=>"Day Dream, ", "PublisherText"=>{"publisher_id"=>"day_dream", "__content__"=>"Day Dream"}, "Details"=>{"change_time"=>"2012-01-17T22:23:43Z", "price_time"=>"2012-03-04T05:32:03Z", "edition_info"=>"Calendar; 2011-07-01", "language"=>"", "physical_description_text"=>"3.5\"x6.2\"x0.0\"; 0.1 lb", "lcc_number"=>"", "dewey_decimal_normalized"=>"636", "dewey_decimal"=>"636"}}, {"book_id"=>"2012_hello_kitty_juvenile_activity_calendar", "isbn"=>"1423811194", "isbn13"=>"9781423811190", "Title"=>"2012 Hello Kitty Juvenile Activity Calendar", "TitleLong"=>nil, "AuthorsText"=>"Day Dream, ", "PublisherText"=>{"publisher_id"=>"day_dream", "__content__"=>"Day Dream"}, "Details"=>{"change_time"=>"2012-05-15T00:52:54Z", "price_time"=>"2012-06-16T20:10:13Z", "edition_info"=>"Calendar; 2011-07-01", "language"=>"", "physical_description_text"=>"11.0\"x12.0\"x0.3\"; 0.8 lb", "lcc_number"=>"", "dewey_decimal_normalized"=>"741", "dewey_decimal"=>"741"}}, {"book_id"=>"2012_hello_kitty_mini_calendar", "isbn"=>"1423809165", "isbn13"=>"9781423809166", "Title"=>"2012 Hello Kitty Mini Calendar", "TitleLong"=>nil, "AuthorsText"=>"Day Dream, ", "PublisherText"=>{"publisher_id"=>"day_dream", "__content__"=>"Day Dream"}, "Details"=>{"change_time"=>"2012-01-17T22:24:12Z", "price_time"=>"2012-02-09T06:11:01Z", "edition_info"=>"Calendar; 2011-07-01", "language"=>"", "physical_description_text"=>"6.2\"x6.9\"x0.2\"; 0.1 lb", "lcc_number"=>"", "dewey_decimal_normalized"=>"741", "dewey_decimal"=>"741"}}, {"book_id"=>"2012_hello_kitty_wall_calendar", "isbn"=>"1423809696", "isbn13"=>"9781423809692", "Title"=>"2012 Hello Kitty Wall Calendar", "TitleLong"=>nil, "AuthorsText"=>"Day Dream, ", "PublisherText"=>{"publisher_id"=>"day_dream", "__content__"=>"Day Dream"}, "Details"=>{"change_time"=>"2012-01-17T22:14:27Z", "price_time"=>"2012-02-09T06:02:14Z", "edition_info"=>"Calendar; 2011-07-01", "language"=>"", "physical_description_text"=>"10.6\"x11.8\"x0.2\"; 0.5 lb", "lcc_number"=>"", "dewey_decimal_normalized"=>"741", "dewey_decimal"=>"741"}}, {"book_id"=>"2012_hello_kitty_weekly_engagement_calendar", "isbn"=>"1423809092", "isbn13"=>"9781423809098", "Title"=>"2012 Hello Kitty Weekly Engagement Calendar", "TitleLong"=>nil, "AuthorsText"=>"Day Dream, ", "PublisherText"=>{"publisher_id"=>"day_dream", "__content__"=>"Day Dream"}, "Details"=>{"change_time"=>"2012-01-17T22:14:34Z", "price_time"=>"2012-02-08T12:37:05Z", "edition_info"=>"Calendar; 2011-07-01", "language"=>"", "physical_description_text"=>"7.2\"x8.5\"x0.9\"; 1.0 lb", "lcc_number"=>"", "dewey_decimal_normalized"=>"741", "dewey_decimal"=>"741"}}, {"book_id"=>"2_grrrls_hello_gorgeous", "isbn"=>"0439187370", "isbn13"=>"9780439187374", "Title"=>"Hello gorgeous", "TitleLong"=>"Hello gorgeous: a guide to style", "AuthorsText"=>"by Kristen Kemp", "PublisherText"=>{"publisher_id"=>"scholastic", "__content__"=>"New York : Scholastic, c2000."}, "Details"=>{"change_time"=>"2009-09-29T18:09:13Z", "price_time"=>"2011-03-25T22:20:05Z", "edition_info"=>"(pbk.) :$3.99", "language"=>"eng", "physical_description_text"=>"64 p. : ill. (some col.) ; 20 cm.", "lcc_number"=>"", "dewey_decimal_normalized"=>"", "dewey_decimal"=>""}}]}} 101 | end 102 | it_behaves_like "AProperResultSet" do 103 | end 104 | end 105 | 106 | 107 | describe "SingleISBNdb::ResultSet" do 108 | before do 109 | stub_request(:get, /http:\/\/isbndb\.com\/api\/books\.xml\?access_key=ABC123&index1=title(&page_number=(.+))?&results=details&value1=hello/).to_return(:body => File.new('spec/responses/single_book.xml'), :headers => {'Content-Type'=>'text/xml'}) 110 | @result_set = ISBNdb::ResultSet.new('/books.xml?access_key=ABC123&index1=title&results=details&value1=hello', :books) 111 | @expected_parsed_response = {"server_time"=>"2012-06-16T20:10:13Z", "BookList"=>{"total_results"=>"1664", "page_size"=>"10", "page_number"=>"1", "shown_results"=>"1", "BookData"=>{"book_id"=>"100th_day_of_school_a04", "isbn"=>"1590543947", "isbn13"=>"9781590543948", "Title"=>"100th Day of School", "TitleLong"=>"100th Day of School (Hello Reader Level 2)", "AuthorsText"=>"Angela Shelf Medearis, ", "PublisherText"=>{"publisher_id"=>"fitzgerald_books", "__content__"=>"Fitzgerald Books"}, "Details"=>{"change_time"=>"2011-03-08T18:28:45Z", "price_time"=>"2012-05-29T16:45:46Z", "edition_info"=>"Unknown Binding; 2007-01", "language"=>"", "physical_description_text"=>"6.0\"x9.0\"x0.5\"; 0.4 lb; 32 pages", "lcc_number"=>"", "dewey_decimal_normalized"=>"", "dewey_decimal"=>""}}}} 112 | @expected_result_size = 1 113 | end 114 | it_behaves_like "AProperResultSet" do 115 | end 116 | end 117 | 118 | -------------------------------------------------------------------------------- /spec/units/result_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ISBNdb::Result do 4 | context 'initialize' do 5 | it 'should set the @store instance variable' do 6 | ISBNdb::Result.new.instance_variable_get('@store').should_not be_nil 7 | end 8 | end 9 | 10 | context 'instance_methods' do 11 | context 'when the hash uses symbols for keys' do 12 | it 'should return an array of all methods' do 13 | ISBNdb::Result.new({ :foo => 'bar', :zip => 'zap' }).instance_methods.should == ['foo', 'zip'] 14 | end 15 | end 16 | 17 | context 'when the hash is empty' do 18 | it 'should return an empty array' do 19 | ISBNdb::Result.new.instance_methods.should be_empty 20 | end 21 | end 22 | end 23 | 24 | context 'to_s' do 25 | context 'with no instance_methods' do 26 | it 'should return the correct string' do 27 | ISBNdb::Result.new.to_s.should == '#' 28 | end 29 | end 30 | 31 | context 'with instance_methods' do 32 | it 'should return the correct string' do 33 | ISBNdb::Result.new({ :foo => 'bar', :zip => 'zap' }).to_s.should == '#' 34 | end 35 | end 36 | end 37 | 38 | context 'inspect' do 39 | context 'with no instance_methods' do 40 | it 'should return the correct string' do 41 | ISBNdb::Result.new.inspect.should == '#' 42 | end 43 | end 44 | 45 | context 'with instance_methods' do 46 | it 'should return the correct string' do 47 | ISBNdb::Result.new({ :foo => 'bar', :zip => 'zap' }).inspect.should == '# "bar", :zip => "zap">' 48 | end 49 | end 50 | end 51 | 52 | context 'build_result' do 53 | before do 54 | @result = ISBNdb::Result.new 55 | end 56 | 57 | context 'with cAmElCaSeD keys' do 58 | it 'should covert them to underscored keys' do 59 | @result.send(:build_result, { 'FooBar' => 'yep', 'ZipZap' => 'nope' }).should == { 'foo_bar' => 'yep', 'zip_zap' => 'nope' } 60 | end 61 | end 62 | 63 | context 'with under_scored keys' do 64 | it 'should not convert the keys' do 65 | @result.send(:build_result, { 'foo_bar' => 'yep', 'zip_zap' => 'nope' }).should == { 'foo_bar' => 'yep', 'zip_zap' => 'nope' } 66 | end 67 | end 68 | 69 | context 'with symbols for keys' do 70 | it 'should convert the keys to strings' do 71 | @result.send(:build_result, { :foo => 'bar', :zip => 'zap' }).should == { 'foo' => 'bar', 'zip' => 'zap' } 72 | end 73 | end 74 | 75 | context 'with strings for keys' do 76 | it 'not alter the keys' do 77 | @result.send(:build_result, { 'foo' => 'bar', 'zip' => 'zap' }).should == { 'foo' => 'bar', 'zip' => 'zap' } 78 | end 79 | end 80 | 81 | context 'with symbols as strings for keys' do 82 | it 'should convert the keys to strings' do 83 | @result.send(:build_result, { :'foo' => 'bar', :'zip' => 'zap' }).should == { 'foo' => 'bar', 'zip' => 'zap' } 84 | end 85 | end 86 | 87 | context 'with an empty hash' do 88 | it 'should return an empty hash' do 89 | @result.send(:build_result, {}).should be_empty 90 | end 91 | end 92 | 93 | context 'with a flat hash' do 94 | it 'should return the correct hash' do 95 | @result.send(:build_result, { :foo => 'bar', :zip => 'zap' }).should == { 'foo' => 'bar', 'zip' => 'zap' } 96 | end 97 | end 98 | 99 | context 'with a nested hash' do 100 | it 'should convert nested levels' do 101 | @result.send(:build_result, { 'foo' => { :bar => 'zip', 'LeftRight' => 'blue', :'MonkeyPatch' => 'true' } }).should == { 'foo' => { 'bar' => 'zip', 'left_right' => 'blue', 'monkey_patch' => 'true' } } 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /spec/units/string_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe String do 4 | context 'is_plural?' do 5 | it 'should return true if the string is plural' do 6 | 'authors'.is_plural?.should be_true 7 | 'books'.is_plural?.should be_true 8 | 'subjects'.is_plural?.should be_true 9 | 'categories'.is_plural?.should be_true 10 | 'publishers'.is_plural?.should be_true 11 | end 12 | 13 | it 'should return false if the string is not plural' do 14 | 'author'.is_plural?.should_not be_true 15 | 'book'.is_plural?.should_not be_true 16 | 'subject'.is_plural?.should_not be_true 17 | 'category'.is_plural?.should_not be_true 18 | 'publisher'.is_plural?.should_not be_true 19 | end 20 | end 21 | 22 | context 'is_singular?' do 23 | it 'should return true if the string is singular' do 24 | 'author'.is_singular?.should be_true 25 | 'book'.is_singular?.should be_true 26 | 'subject'.is_singular?.should be_true 27 | 'category'.is_singular?.should be_true 28 | 'publisher'.is_singular?.should be_true 29 | end 30 | 31 | it 'should return false if the string is not singular' do 32 | 'authors'.is_singular?.should_not be_true 33 | 'books'.is_singular?.should_not be_true 34 | 'subjects'.is_singular?.should_not be_true 35 | 'categories'.is_singular?.should_not be_true 36 | 'publishers'.is_singular?.should_not be_true 37 | end 38 | end 39 | 40 | context 'titleize' do 41 | it 'should capitalize the first letter' do 42 | 'author'.titleize.should == 'Author' 43 | 'book'.titleize.should == 'Book' 44 | 'subject'.titleize.should == 'Subject' 45 | 'category'.titleize.should == 'Category' 46 | 'publisher'.titleize.should == 'Publisher' 47 | end 48 | 49 | it 'should do nothing if the letter is already capitalized' do 50 | 'Author'.titleize.should == 'Author' 51 | 'Book'.titleize.should == 'Book' 52 | 'Subject'.titleize.should == 'Subject' 53 | 'Category'.titleize.should == 'Category' 54 | 'Publisher'.titleize.should == 'Publisher' 55 | end 56 | end 57 | 58 | context 'singularize' do 59 | it 'should return the singular form of a word' do 60 | 'authors'.singularize.should == 'author' 61 | 'books'.singularize.should == 'book' 62 | 'subjects'.singularize.should == 'subject' 63 | 'categories'.singularize.should == 'category' 64 | 'publishers'.singularize.should == 'publisher' 65 | end 66 | 67 | it 'should not alter an already singular word' do 68 | 'author'.singularize.should == 'author' 69 | 'book'.singularize.should == 'book' 70 | 'subject'.singularize.should == 'subject' 71 | 'category'.singularize.should == 'category' 72 | 'publisher'.singularize.should == 'publisher' 73 | end 74 | end 75 | 76 | context 'pluralize' do 77 | it 'should return the plural form of a word' do 78 | 'author'.pluralize.should == 'authors' 79 | 'book'.pluralize.should == 'books' 80 | 'subject'.pluralize.should == 'subjects' 81 | 'category'.pluralize.should == 'categories' 82 | 'publisher'.pluralize.should == 'publishers' 83 | end 84 | 85 | it 'should not alter an already singular word' do 86 | 'authors'.pluralize.should == 'authors' 87 | 'books'.pluralize.should == 'books' 88 | 'subjects'.pluralize.should == 'subjects' 89 | 'categories'.pluralize.should == 'categories' 90 | 'publishers'.pluralize.should == 'publishers' 91 | end 92 | end 93 | 94 | context 'blank?' do 95 | it 'should return true for the empty string' do 96 | ''.blank?.should be_true 97 | ' '.blank?.should be_true 98 | end 99 | 100 | it 'should not return true for non-empty strings' do 101 | 'hello'.blank?.should_not be_true 102 | end 103 | end 104 | 105 | context 'underscore' do 106 | it 'should convert the camel cased word to underscores' do 107 | 'HelloWorld'.underscore.should == 'hello_world' 108 | 'Cookie'.underscore.should == 'cookie' 109 | 'AReallyLongStringThatMightConfuseTheMethod'.underscore.should == 'a_really_long_string_that_might_confuse_the_method' 110 | end 111 | 112 | it 'should not change already underscored words' do 113 | 'hello_world'.underscore.should == 'hello_world' 114 | 'cookie'.underscore.should == 'cookie' 115 | 'a_really_long_string_that_might_confuse_the_method'.underscore.should == 'a_really_long_string_that_might_confuse_the_method' 116 | end 117 | end 118 | end 119 | --------------------------------------------------------------------------------