bol 0.0.1.beta
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/CHANGELOG.md +1 -0
- data/Gemfile +2 -0
- data/Guardfile +7 -0
- data/LICENSE.md +19 -0
- data/README.md +170 -0
- data/Rakefile +6 -0
- data/bol.gemspec +25 -0
- data/lib/bol.rb +52 -0
- data/lib/bol/category.rb +5 -0
- data/lib/bol/configuration.rb +55 -0
- data/lib/bol/parser.rb +34 -0
- data/lib/bol/parsers.rb +7 -0
- data/lib/bol/parsers/categories.rb +17 -0
- data/lib/bol/parsers/products.rb +34 -0
- data/lib/bol/parsers/refinements.rb +24 -0
- data/lib/bol/product.rb +51 -0
- data/lib/bol/proxy.rb +37 -0
- data/lib/bol/query.rb +79 -0
- data/lib/bol/refinement.rb +5 -0
- data/lib/bol/refinement_group.rb +5 -0
- data/lib/bol/request.rb +90 -0
- data/lib/bol/requests.rb +7 -0
- data/lib/bol/requests/list.rb +26 -0
- data/lib/bol/requests/product.rb +16 -0
- data/lib/bol/requests/search.rb +11 -0
- data/lib/bol/scope.rb +55 -0
- data/lib/bol/signature.rb +44 -0
- data/lib/bol/version.rb +3 -0
- data/spec/bol/configuration_spec.rb +46 -0
- data/spec/bol/parsers/products_spec.rb +77 -0
- data/spec/bol/product_spec.rb +71 -0
- data/spec/bol/proxy_spec.rb +34 -0
- data/spec/bol/query_spec.rb +129 -0
- data/spec/bol/request_spec.rb +119 -0
- data/spec/bol/scope_spec.rb +84 -0
- data/spec/bol/signature_spec.rb +27 -0
- data/spec/bol_spec.rb +55 -0
- data/spec/fixtures/categorylist.xml +232 -0
- data/spec/fixtures/productlists.xml +164 -0
- data/spec/fixtures/products.xml +72 -0
- data/spec/fixtures/searchproducts-music.xml +108 -0
- data/spec/fixtures/searchproducts.xml +322 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support.rb +6 -0
- metadata +177 -0
data/.gitignore
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
(unreleased)
|
data/Gemfile
ADDED
data/Guardfile
ADDED
data/LICENSE.md
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
data/bol.gemspec
ADDED
@@ -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
|
data/lib/bol.rb
ADDED
@@ -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
|
data/lib/bol/category.rb
ADDED
@@ -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
|
data/lib/bol/parser.rb
ADDED
@@ -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
|
data/lib/bol/parsers.rb
ADDED
@@ -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
|