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