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.
- 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
|