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