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
@@ -0,0 +1,24 @@
|
|
1
|
+
module Bol
|
2
|
+
module Parsers
|
3
|
+
class Refinements < Parser
|
4
|
+
def xpath
|
5
|
+
'*/RefinementGroup'
|
6
|
+
end
|
7
|
+
|
8
|
+
def parse_object(el)
|
9
|
+
RefinementGroup.new.tap do |category|
|
10
|
+
category.name = el.elements['Name'].text.strip
|
11
|
+
category.id = el.elements['Id'].text.strip
|
12
|
+
category.refinements = []
|
13
|
+
el.elements.each('Refinement') do |e|
|
14
|
+
category.refinements << Refinement.new.tap { |r|
|
15
|
+
r.name = e.elements['Name'].text.strip
|
16
|
+
r.id = e.elements['Id'].text.strip
|
17
|
+
r.count = e.elements['ProductCount'].text.strip
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/bol/product.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module Bol
|
4
|
+
class Product
|
5
|
+
def self.find(id)
|
6
|
+
Requests::Product.new(id, Query.new(0)).proxy.all.first
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :attributes
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@attributes = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def [](key)
|
16
|
+
@attributes[key]
|
17
|
+
end
|
18
|
+
|
19
|
+
def []=(key, value)
|
20
|
+
@attributes[key] = value
|
21
|
+
end
|
22
|
+
|
23
|
+
def cover(kind = :medium)
|
24
|
+
attributes[:cover].fetch(kind)
|
25
|
+
end
|
26
|
+
|
27
|
+
def referral_url(site_id)
|
28
|
+
format = "http://partnerprogramma.bol.com/click/click?p=1&t=url&s=%s&url=%s&f=API&subid=%s&name=%s"
|
29
|
+
format % [
|
30
|
+
site_id,
|
31
|
+
attributes.fetch(:url),
|
32
|
+
attributes.fetch(:id),
|
33
|
+
attributes.fetch(:title),
|
34
|
+
].map { |p| CGI.escape(p) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def method_missing(name, *args)
|
38
|
+
return super unless attributes.keys.include?(name)
|
39
|
+
if name =~ /=$/
|
40
|
+
attributes[name] = *args
|
41
|
+
else
|
42
|
+
attributes[name]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def respond_to?(name)
|
47
|
+
super or attributes.keys.include?(name) or
|
48
|
+
attributes.keys.include?(name.to_s.sub(/=$/, '').to_sym)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/bol/proxy.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
module Bol
|
2
|
+
class Proxy
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
attr_reader :request, :parser
|
6
|
+
|
7
|
+
def initialize(request, parser)
|
8
|
+
@request = request
|
9
|
+
@parser = parser
|
10
|
+
end
|
11
|
+
|
12
|
+
def each
|
13
|
+
all.each { |a| yield a }
|
14
|
+
end
|
15
|
+
|
16
|
+
def all
|
17
|
+
@all ||= parser.objects
|
18
|
+
end
|
19
|
+
|
20
|
+
def query
|
21
|
+
request.query
|
22
|
+
end
|
23
|
+
|
24
|
+
%w[category_id= category_id].each do |method|
|
25
|
+
define_method method do |*args|
|
26
|
+
query.send method, *args
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
%w[order page offset limit].each do |method|
|
31
|
+
define_method method do |*args|
|
32
|
+
query.send method, *args
|
33
|
+
self
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/bol/query.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
module Bol
|
2
|
+
class Query
|
3
|
+
attr_reader :category_id
|
4
|
+
attr_accessor :request
|
5
|
+
|
6
|
+
def initialize(category_id)
|
7
|
+
raise ArgumentError if category_id =~ /[^\d+]/
|
8
|
+
@category_id = category_id
|
9
|
+
@inclusions = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def params
|
13
|
+
{ categoryId: @category_id }.tap do |p|
|
14
|
+
p[:nrProducts] = @limit if @limit
|
15
|
+
p[:offset] = @offset if @offset
|
16
|
+
p[:sortingMethod] = @order_key if @order_key
|
17
|
+
p[:sortingAscending] = @order_direction if @order_direction
|
18
|
+
p[:term] = @term if @term
|
19
|
+
p[:includeCategories] = categories?
|
20
|
+
p[:includeProducts] = products?
|
21
|
+
p[:includeRefinements] = refinements?
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def has_param?(key)
|
26
|
+
params.has_key?(key) and !params[key].nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
def categories?
|
30
|
+
@inclusions.include? :categories
|
31
|
+
end
|
32
|
+
|
33
|
+
def products?
|
34
|
+
@inclusions.include? :products
|
35
|
+
end
|
36
|
+
|
37
|
+
def refinements?
|
38
|
+
@inclusions.include? :refinements
|
39
|
+
end
|
40
|
+
|
41
|
+
def include(kind)
|
42
|
+
@inclusions << kind
|
43
|
+
end
|
44
|
+
|
45
|
+
def search(term = nil)
|
46
|
+
return @term if term.nil?
|
47
|
+
@term = term
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
def order(str)
|
52
|
+
if str =~ /^(sales_ranking|price|title|publishing_date|customer_rating) (ASC|DESC)/
|
53
|
+
@order_key = $1
|
54
|
+
@order_direction = $2 == 'ASC' ? 'true' : 'false'
|
55
|
+
else
|
56
|
+
raise ArgumentError
|
57
|
+
end
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
def page(n = nil)
|
62
|
+
limit Bol.configuration[:per_page]
|
63
|
+
offset ((n || 1).to_i - 1) * Bol.configuration[:per_page]
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
def offset(n = nil)
|
68
|
+
return @offset if n.nil?
|
69
|
+
@offset = n.to_i
|
70
|
+
self
|
71
|
+
end
|
72
|
+
|
73
|
+
def limit(n = nil)
|
74
|
+
return @limit if n.nil?
|
75
|
+
@limit = n.to_i
|
76
|
+
self
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/bol/request.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'net/https'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module Bol
|
6
|
+
NotFound = Class.new(Exception)
|
7
|
+
|
8
|
+
class Request
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
attr_reader :query, :response, :proxy
|
12
|
+
|
13
|
+
DOMAIN = 'openapi.bol.com'
|
14
|
+
|
15
|
+
def_delegators :response, :code, :body
|
16
|
+
|
17
|
+
def self.ignore_params(*args)
|
18
|
+
@params_to_ignore ||= []
|
19
|
+
[*args].each do |arg|
|
20
|
+
@params_to_ignore << arg.to_sym
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.ignored_param?(name)
|
25
|
+
@params_to_ignore ||= []
|
26
|
+
@params_to_ignore.include?(name.to_sym)
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(query)
|
30
|
+
@query = query
|
31
|
+
@query.request = self
|
32
|
+
end
|
33
|
+
|
34
|
+
def proxy
|
35
|
+
@proxy ||= Proxy.new(self, Parsers::Products.new(self))
|
36
|
+
end
|
37
|
+
|
38
|
+
def params
|
39
|
+
@params ||= query.params.reject do |k, v|
|
40
|
+
self.class.ignored_param?(k)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def query_string
|
45
|
+
@query_string ||= params.map { |k, v|
|
46
|
+
"#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}"
|
47
|
+
}.join('&')
|
48
|
+
end
|
49
|
+
|
50
|
+
def uri
|
51
|
+
@uri ||= begin
|
52
|
+
uri = if params.empty?
|
53
|
+
"https://#{DOMAIN}#{path}"
|
54
|
+
else
|
55
|
+
"https://#{DOMAIN}#{path}?#{query_string}"
|
56
|
+
end
|
57
|
+
URI.parse(uri)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def fetch
|
62
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
63
|
+
http.use_ssl = true
|
64
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
65
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
66
|
+
request.content_type = 'application/xml'
|
67
|
+
request['Connection'] = 'close'
|
68
|
+
request['X-OpenAPI-Authorization'] = Signature.new(date, path, params).generate
|
69
|
+
request['X-OpenAPI-Date'] = date
|
70
|
+
@response = http.request(request)
|
71
|
+
raise Bol::NotFound if @response.code =~ /^4\d\d$/
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
def success?
|
76
|
+
response.code == '200'
|
77
|
+
end
|
78
|
+
|
79
|
+
protected
|
80
|
+
|
81
|
+
def date
|
82
|
+
@date ||= Time.now.utc.strftime '%a, %d %B %Y %H:%M:%S GMT'
|
83
|
+
end
|
84
|
+
|
85
|
+
# overridden in subclasses
|
86
|
+
def path
|
87
|
+
'/'
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
data/lib/bol/requests.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Bol
|
2
|
+
module Requests
|
3
|
+
class List < Request
|
4
|
+
def initialize(type, *args)
|
5
|
+
@type = type
|
6
|
+
super(*args)
|
7
|
+
end
|
8
|
+
|
9
|
+
def path
|
10
|
+
"/openapi/services/rest/catalog/v3/listresults/#{@type}/#{@query.params[:categoryId]}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def proxy
|
14
|
+
@proxy ||= begin
|
15
|
+
if @query.categories?
|
16
|
+
Proxy.new(self, Parsers::Categories.new(self))
|
17
|
+
elsif @query.refinements?
|
18
|
+
Proxy.new(self, Parsers::Refinements.new(self))
|
19
|
+
elsif @query.products?
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Bol
|
2
|
+
module Requests
|
3
|
+
class Product < Request
|
4
|
+
ignore_params :includeCategories, :includeProducts, :includeRefinements
|
5
|
+
|
6
|
+
def initialize(product_id, *args)
|
7
|
+
@product_id = product_id
|
8
|
+
super *args
|
9
|
+
end
|
10
|
+
|
11
|
+
def path
|
12
|
+
"/openapi/services/rest/catalog/v3/products/#{URI.escape(@product_id.to_s)}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/bol/scope.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
module Bol
|
2
|
+
class Scope
|
3
|
+
attr_reader :ids
|
4
|
+
|
5
|
+
def initialize(ids = nil)
|
6
|
+
@ids = Array(ids)
|
7
|
+
@ids = [0] if @ids.empty?
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_s
|
11
|
+
ids.join('+')
|
12
|
+
end
|
13
|
+
|
14
|
+
def +(other)
|
15
|
+
self.class.new((ids + other.ids).uniq)
|
16
|
+
end
|
17
|
+
|
18
|
+
def -(other)
|
19
|
+
self.class.new(ids - other.ids)
|
20
|
+
end
|
21
|
+
|
22
|
+
def search(terms)
|
23
|
+
q = Query.new(to_s)
|
24
|
+
q.search terms
|
25
|
+
Requests::Search.new(q).proxy
|
26
|
+
end
|
27
|
+
|
28
|
+
def categories
|
29
|
+
q = Query.new(to_s)
|
30
|
+
q.include :categories
|
31
|
+
Requests::List.new('toplist_default', q).proxy
|
32
|
+
end
|
33
|
+
|
34
|
+
def refinements
|
35
|
+
q = Query.new(to_s)
|
36
|
+
q.include :refinements
|
37
|
+
Requests::List.new('toplist_default', q).proxy
|
38
|
+
end
|
39
|
+
|
40
|
+
{
|
41
|
+
top_products: 'toplist_default',
|
42
|
+
top_products_overall: 'toplist_overall',
|
43
|
+
top_products_last_week: 'toplist_last_week',
|
44
|
+
top_products_last_two_months: 'toplist_last_two_months',
|
45
|
+
new_products: 'new',
|
46
|
+
preorder_products: 'preorder'
|
47
|
+
}.each_pair do |method, type|
|
48
|
+
define_method method do
|
49
|
+
q = Query.new(to_s)
|
50
|
+
q.include :products
|
51
|
+
Requests::List.new(type, q).proxy
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module Bol
|
4
|
+
class Signature
|
5
|
+
attr_reader :date, :path, :params, :key, :secret
|
6
|
+
|
7
|
+
def initialize(date, path, params)
|
8
|
+
@date, @path, @params = date, path, params
|
9
|
+
Bol.configuration.validate
|
10
|
+
@key = Bol.configuration[:access_key]
|
11
|
+
@secret = Bol.configuration[:secret]
|
12
|
+
end
|
13
|
+
|
14
|
+
def generate
|
15
|
+
key + ':' + encoded_hash
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def encoded_hash
|
21
|
+
Base64.encode64(hash).sub /\n/, ''
|
22
|
+
end
|
23
|
+
|
24
|
+
def hash
|
25
|
+
OpenSSL::HMAC.digest('sha256', secret, headers)
|
26
|
+
end
|
27
|
+
|
28
|
+
def headers
|
29
|
+
[
|
30
|
+
'GET',
|
31
|
+
'',
|
32
|
+
'application/xml',
|
33
|
+
date,
|
34
|
+
"x-openapi-date:#{date}",
|
35
|
+
path,
|
36
|
+
signature_params
|
37
|
+
].join("\n")
|
38
|
+
end
|
39
|
+
|
40
|
+
def signature_params
|
41
|
+
params.map { |k, v| "&#{k}=#{v}" }.compact.sort.join("\n")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|