bol 0.0.1.beta

Sign up to get free protection for your applications and to get access to all the features.
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