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.
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,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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ module Bol
2
+ class Refinement
3
+ attr_accessor :name, :id, :count
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Bol
2
+ class RefinementGroup
3
+ attr_accessor :name, :id, :refinements
4
+ end
5
+ end
@@ -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
@@ -0,0 +1,7 @@
1
+ module Bol
2
+ module Requests
3
+ autoload :Product, 'bol/requests/product'
4
+ autoload :Search, 'bol/requests/search'
5
+ autoload :List, 'bol/requests/list'
6
+ end
7
+ end
@@ -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
@@ -0,0 +1,11 @@
1
+ module Bol
2
+ module Requests
3
+ class Search < Request
4
+ ignore_params :includeCategories, :includeProducts, :includeRefinements
5
+
6
+ def path
7
+ "/openapi/services/rest/catalog/v3/searchresults"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -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