bol 0.0.1.beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. data/.gitignore +4 -0
  2. data/CHANGELOG.md +1 -0
  3. data/Gemfile +2 -0
  4. data/Guardfile +7 -0
  5. data/LICENSE.md +19 -0
  6. data/README.md +170 -0
  7. data/Rakefile +6 -0
  8. data/bol.gemspec +25 -0
  9. data/lib/bol.rb +52 -0
  10. data/lib/bol/category.rb +5 -0
  11. data/lib/bol/configuration.rb +55 -0
  12. data/lib/bol/parser.rb +34 -0
  13. data/lib/bol/parsers.rb +7 -0
  14. data/lib/bol/parsers/categories.rb +17 -0
  15. data/lib/bol/parsers/products.rb +34 -0
  16. data/lib/bol/parsers/refinements.rb +24 -0
  17. data/lib/bol/product.rb +51 -0
  18. data/lib/bol/proxy.rb +37 -0
  19. data/lib/bol/query.rb +79 -0
  20. data/lib/bol/refinement.rb +5 -0
  21. data/lib/bol/refinement_group.rb +5 -0
  22. data/lib/bol/request.rb +90 -0
  23. data/lib/bol/requests.rb +7 -0
  24. data/lib/bol/requests/list.rb +26 -0
  25. data/lib/bol/requests/product.rb +16 -0
  26. data/lib/bol/requests/search.rb +11 -0
  27. data/lib/bol/scope.rb +55 -0
  28. data/lib/bol/signature.rb +44 -0
  29. data/lib/bol/version.rb +3 -0
  30. data/spec/bol/configuration_spec.rb +46 -0
  31. data/spec/bol/parsers/products_spec.rb +77 -0
  32. data/spec/bol/product_spec.rb +71 -0
  33. data/spec/bol/proxy_spec.rb +34 -0
  34. data/spec/bol/query_spec.rb +129 -0
  35. data/spec/bol/request_spec.rb +119 -0
  36. data/spec/bol/scope_spec.rb +84 -0
  37. data/spec/bol/signature_spec.rb +27 -0
  38. data/spec/bol_spec.rb +55 -0
  39. data/spec/fixtures/categorylist.xml +232 -0
  40. data/spec/fixtures/productlists.xml +164 -0
  41. data/spec/fixtures/products.xml +72 -0
  42. data/spec/fixtures/searchproducts-music.xml +108 -0
  43. data/spec/fixtures/searchproducts.xml +322 -0
  44. data/spec/spec_helper.rb +16 -0
  45. data/spec/support.rb +6 -0
  46. metadata +177 -0
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
@@ -0,0 +1 @@
1
+ (unreleased)
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
@@ -0,0 +1,7 @@
1
+ guard 'rspec', version: 2 do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ watch('spec/support.rb') { "spec" }
6
+ end
7
+
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2011 by Arjan van der Gaag
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -0,0 +1,170 @@
1
+ # Bol
2
+
3
+ A Ruby wrapper around the [bol.com developers API][docs], that will be made
4
+ available as a Gem. *Currently in beta stage.*
5
+
6
+ [docs]: http://developers.bol.com
7
+
8
+ ## Installation
9
+
10
+ Bol is a simple Ruby gem, so it requires a working installation of Ruby with
11
+ Ruby gems. Ruby 1.9 is required. Install the gem:
12
+
13
+ ```
14
+ $ gem install bol
15
+ ```
16
+
17
+ Or, if your project uses [Bundler][] simply add it to your `Gemfile`:
18
+
19
+ [Bundler]: http://gembundler.com
20
+
21
+ ```ruby
22
+ gem 'bol'
23
+ ```
24
+
25
+ Then, simply `require` it in your code, provide some configuration settings and
26
+ query away.
27
+
28
+ ## Available operations
29
+
30
+ Here are the currently working operations:
31
+
32
+ ### Loading a specific product
33
+
34
+ If you know an ID, you can load a product directly:
35
+
36
+ ```ruby
37
+ product = Bol::Product.find(params[:id])
38
+ product.title
39
+ product.cover(:medium)
40
+ product.referral_url('my_associate_id')
41
+ ```
42
+
43
+ ### Listing products
44
+
45
+ You can get a list of popular or bestselling products:
46
+
47
+ * `Bol.top_products`
48
+ * `Bol.top_products_overall`
49
+ * `Bol.top_products_last_week`
50
+ * `Bol.top_products_last_two_months`
51
+ * `Bol.new_products`
52
+ * `Bol.preorder_products`
53
+
54
+ Or, you can apply a scope to limit results to a category:
55
+
56
+ ```ruby
57
+ Bol::Scope.new(params[:category_id]).top_producs
58
+ ```
59
+
60
+ ### Searching products
61
+
62
+ You can search globally for keywords or ISBN and use a Arel-like syntax
63
+ for setting options:
64
+
65
+ ```ruby
66
+ Bol.search(params[:query]).limit(10).offset(10).order('sales_ranking ASC')
67
+ Bol.search(params[:query]).page(params[:page])
68
+ ```
69
+
70
+ You can scope your search to a specific category:
71
+
72
+ ```ruby
73
+ Bol::Scope.new(params[:category_id]).search(params[:query])
74
+ ```
75
+
76
+ ### Loading categories and refinements
77
+
78
+ Loading all top-level categories (e.g. `DVDs` or `English Book`) is simple
79
+ enough:
80
+
81
+ ```ruby
82
+ categories = Bol.categories
83
+ categories.first.name # => 'Books'
84
+ ```
85
+
86
+ You can load subsequent subcategories:
87
+
88
+ ```ruby
89
+ Bol::Scope.new(categories.first.id).categories
90
+ ```
91
+
92
+ Refinements (e.g. 'under 10 euros') work much the same way as categories, but
93
+ are grouped under a shared name, such as group 'Price' with refinements 'up to
94
+ 10 euros', '10 to 20 euros', etc.:
95
+
96
+ ```ruby
97
+ groups = Bol.refinements
98
+ group = groups.first
99
+ group.name # => 'Price'
100
+ group.refinements.first.name # => 'under 10 euros'
101
+ ```
102
+
103
+ ### Scoping operations
104
+
105
+ The `Bol::Scope` object limits results to given categories and/or refinements.
106
+ You can create a scope using explicit IDs, and you can do basic combinations:
107
+
108
+ ```ruby
109
+ books = Bol::Scope.new(some_category_id)
110
+ cheap = Bol::Scope.new(some_refinement_id)
111
+ (books + cheap).top_products
112
+ ```
113
+
114
+ Here's an overview of all the operations that should still be implemented:
115
+
116
+ ## Background
117
+
118
+ The available operations map almost directly to operations provided by the API
119
+ to search, load lists of products or load a single product by ID. I do aim to
120
+ a add a little sugar to make working with Ruby objects a little easier:
121
+
122
+ * Add `page` helper method to combine `limit` and `offset`
123
+ * Scope operations by category in a ActiveRecord association style
124
+ * Delay API calls until explicitly requested or triggered by looping over
125
+ results
126
+
127
+ ## Wishlist
128
+
129
+ * Allow scoping by category or refinement objects instead of just IDs
130
+ * Add a simple identiy map, so the same product does not have to be loaded
131
+ twice when requested twice
132
+ * Properly differentiate between product types. Currently built around books;
133
+ DVDs, music and toys may or may not work as expected.
134
+ * Add default ordering of products
135
+
136
+ I do not need this stuff myself, but I will gladly take pull requests for such
137
+ features.
138
+
139
+ ## Configuration
140
+
141
+ To be allowed to make requests to the Bol.com API you need to register on their
142
+ site and request a access key and secret. Configure the Bol gem as follows:
143
+
144
+ ```ruby
145
+ Bol.configure do |c|
146
+ c.key = 'your public access key'
147
+ c.secret = 'your private secret'
148
+ c.per_page = 10
149
+ end
150
+ ```
151
+
152
+ ## Note on Patches/Pull Requests
153
+
154
+ * Fork the project.
155
+ * Make your feature addition or bug fix.
156
+ * Add tests for it. This is important so I don't break it in a future version
157
+ unintentionally.
158
+ * Commit, do not mess with rakefile, version, or history. (if you want to have
159
+ your own version, that is fine but bump version in a commit by itself I can
160
+ ignore when I pull)
161
+ * Send me a pull request. Bonus points for topic branches.
162
+
163
+ ## History
164
+
165
+ For a full list of changes, please see CHANGELOG.md
166
+
167
+ ## License
168
+
169
+ Copyright (C) 2011 by Arjan van der Gaag. Published under the MIT license. See
170
+ LICENSE.md for details.
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+ require 'bol/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'bol'
7
+ s.version = Bol::VERSION
8
+ s.authors = ['Arjan van der Gaag']
9
+ s.email = ['arjan@arjanvandergaag.nl']
10
+ s.homepage = "https://github.com/avdgaag/bol"
11
+ s.summary = %q{Simple Ruby wrapper around the bol.com developer API}
12
+ s.description = %q{Access the bol.com product catalog from a Ruby project. You can search products, list top selling products and find product information for individual catalog items.}
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.require_paths = ['lib']
18
+
19
+ s.add_development_dependency 'rake'
20
+ s.add_development_dependency 'rspec'
21
+ s.add_development_dependency 'guard-rspec'
22
+ s.add_development_dependency 'rb-fsevent'
23
+ s.add_development_dependency 'growl'
24
+ s.add_development_dependency 'fakeweb'
25
+ end
@@ -0,0 +1,52 @@
1
+ require 'bol/version'
2
+
3
+ module Bol
4
+ autoload :Scope, 'bol/scope'
5
+ autoload :Configuration, 'bol/configuration'
6
+ autoload :Product, 'bol/product'
7
+ autoload :Query, 'bol/query'
8
+ autoload :Request, 'bol/request'
9
+ autoload :Requests, 'bol/requests'
10
+ autoload :Parser, 'bol/parser'
11
+ autoload :Parsers, 'bol/parsers'
12
+ autoload :Proxy, 'bol/proxy'
13
+ autoload :Category, 'bol/category'
14
+ autoload :Refinement, 'bol/refinement'
15
+ autoload :RefinementGroup, 'bol/refinement_group'
16
+ autoload :Signature, 'bol/signature'
17
+
18
+ def self.configuration
19
+ @configuration ||= Configuration.new
20
+ end
21
+
22
+ def self.reset_configuration
23
+ @configuration = nil
24
+ end
25
+
26
+ def self.configure(options = nil)
27
+ @configuration = Configuration.new(options)
28
+ yield @configuration if options.nil?
29
+ end
30
+
31
+ class << self
32
+ %w{
33
+ top_products
34
+ top_products_overall
35
+ top_products_last_week
36
+ top_products_last_two_months
37
+ new_products
38
+ preorder_products
39
+ search
40
+ categories
41
+ refinements
42
+ }.each do |name|
43
+ define_method name do |*args|
44
+ Scope.new.send(name, *args)
45
+ end
46
+ end
47
+ end
48
+
49
+ def find(id)
50
+ Bol::Product.find(id)
51
+ end
52
+ end
@@ -0,0 +1,5 @@
1
+ module Bol
2
+ class Category
3
+ attr_accessor :name, :id, :count
4
+ end
5
+ end
@@ -0,0 +1,55 @@
1
+ module Bol
2
+ ConfigurationError = Class.new(Exception)
3
+
4
+ class Configuration
5
+ ALLOWED_KEYS = %w[access_key secret per_page].map(&:to_sym)
6
+ REQUIRED_KEYS = %w[access_key secret].map(&:to_sym)
7
+
8
+ def initialize(options = {})
9
+ unless options.nil? || options.respond_to?(:each_pair)
10
+ raise ArgumentError, 'options should be Hash-like object'
11
+ end
12
+
13
+ @options = { per_page: 10 }
14
+
15
+ unless options.nil?
16
+ options.each_pair do |k, v|
17
+ self[k] = v
18
+ end
19
+ end
20
+ end
21
+
22
+ def [](key)
23
+ @options[key]
24
+ end
25
+
26
+ def []=(key, value)
27
+ unless ALLOWED_KEYS.include?(key)
28
+ raise ArgumentError, "#{key} is not a valid key"
29
+ end
30
+
31
+ @options[key] = value
32
+ end
33
+
34
+ def validate
35
+ REQUIRED_KEYS.each { |key| @options.fetch(key) }
36
+ rescue KeyError
37
+ raise Bol::ConfigurationError
38
+ end
39
+
40
+ def method_missing(name, *args)
41
+ return super unless respond_to? name
42
+ if name.to_s =~ /=$/
43
+ send(:[]=, name.to_s.sub(/=$/, '').to_sym, *args)
44
+ else
45
+ send(:[], name)
46
+ end
47
+ end
48
+
49
+ def respond_to?(name)
50
+ super or
51
+ ALLOWED_KEYS.include?(name) or
52
+ ALLOWED_KEYS.include?(name.to_s.sub(/=$/, '').to_sym)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,34 @@
1
+ require 'rexml/document'
2
+ require 'ostruct'
3
+
4
+ module Bol
5
+ class Parser
6
+ attr_reader :request
7
+
8
+ def initialize(request)
9
+ @request = request
10
+ end
11
+
12
+ def objects
13
+ [].tap do |collection|
14
+ xml.elements.each(xpath) do |el|
15
+ collection << parse_object(el)
16
+ end
17
+ end
18
+ end
19
+
20
+ protected
21
+
22
+ def underscorize(str)
23
+ str.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
24
+ end
25
+
26
+ def camelize(str)
27
+ str.split('_').map(&:capitalize).join('')
28
+ end
29
+
30
+ def xml
31
+ @xml ||= REXML::Document.new(request.fetch.body)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,7 @@
1
+ module Bol
2
+ module Parsers
3
+ autoload :Products, 'bol/parsers/products'
4
+ autoload :Categories, 'bol/parsers/categories'
5
+ autoload :Refinements, 'bol/parsers/refinements'
6
+ end
7
+ end
@@ -0,0 +1,17 @@
1
+ module Bol
2
+ module Parsers
3
+ class Categories < Parser
4
+ def xpath
5
+ '*/Category'
6
+ end
7
+
8
+ def parse_object(el)
9
+ Category.new.tap do |category|
10
+ category.name = el.elements['Name'].text.strip
11
+ category.id = el.elements['Id'].text.strip
12
+ category.count = el.elements['ProductCount'].text.strip.to_i
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,34 @@
1
+ require 'time'
2
+
3
+ module Bol
4
+ module Parsers
5
+ class Products < Parser
6
+ def xpath
7
+ '*/Product'
8
+ end
9
+
10
+ def parse_object(el)
11
+ Product.new.tap do |product|
12
+ %w[id title subtitle type publisher short_description long_description ean rating binding_description language_code language_description].each do |field|
13
+ _field = camelize(field)
14
+ if el.elements[_field]
15
+ product.attributes[field.to_sym] = el.elements[_field].text.gsub(/\n\s+/, ' ').strip
16
+ end
17
+ end
18
+ product[:url] = el.elements['Urls'].elements['Main'].text.strip
19
+ if el.elements['ReleaseDate']
20
+ product[:release_date] = Time.parse(el.elements['ReleaseDate'].to_s)
21
+ end
22
+ product[:authors] = []
23
+ el.elements.each('Authors/Author') do |author|
24
+ product[:authors] << author.elements['Name'].text
25
+ end
26
+ product[:cover] = {}
27
+ el.elements['Images'].elements.each do |image|
28
+ product[:cover][underscorize(image.name).to_sym] = image.text.strip
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end