go_puff-prodcat_api 0.9.3.pre.beta.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cafafb2612024979cba9817f96491e4e22b73130dcefb6c8350115b94ede645e
4
+ data.tar.gz: 0732ad8826d7700e2ae9fc2deb55253898fbbd75779d829ab6c15a0830b95322
5
+ SHA512:
6
+ metadata.gz: 20383f6ef52524106ddd987952c4ca6e621f56fb789e73ad7f28668b37c5258c8a99b61a705e0fa2803614feb9d984becf3b019aca4745f2f3527df51ef19dcb
7
+ data.tar.gz: 0af72f4671b63f556c4f691db1bb454940f96d08454d1667fdb728a7d9cc66c2076df46626e46a3689cb13b96ce92299a05a0900002782d7dd54f1b4d8c6b275
@@ -0,0 +1,3 @@
1
+ ---
2
+ BUNDLE_HTTPS://RUBYGEMS__PKG__GITHUB__COM/GOPUFF/: "GITHUB_USERNAME:PERSONAL_ACCESS_TOKEN"
3
+
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,64 @@
1
+ require:
2
+ - rubocop-thread_safety
3
+ - rubocop-rspec
4
+ - rubocop-performance
5
+
6
+ AllCops:
7
+ NewCops: enable
8
+ TargetRubyVersion: 2.6.6
9
+ SuggestExtensions: false
10
+
11
+ Metrics/ClassLength:
12
+ Enabled: false
13
+
14
+ Metrics/MethodLength:
15
+ Enabled: false
16
+
17
+ Metrics/AbcSize:
18
+ Enabled: false
19
+
20
+ Layout/LineLength:
21
+ Enabled: false
22
+
23
+ Metrics/BlockLength:
24
+ Enabled: false
25
+
26
+ Style/Documentation:
27
+ Enabled: false
28
+
29
+ RSpec/ExampleLength:
30
+ Enabled: false
31
+
32
+ RSpec/NamedSubject:
33
+ Enabled: false
34
+
35
+ RSpec/VerifiedDoubles:
36
+ Enabled: false
37
+
38
+ RSpec/MultipleExpectations:
39
+ Max: 11
40
+
41
+ ThreadSafety/InstanceVariableInClassMethod:
42
+ Exclude:
43
+ - lib/go_puff/prodcat_api.rb
44
+ ThreadSafety/ClassAndModuleAttributes:
45
+ Exclude:
46
+ - lib/go_puff/prodcat_api.rb
47
+ RSpec/AnyInstance:
48
+ Exclude:
49
+ - spec/spec_helper.rb
50
+ - spec/go_puff/prodcat_api/configuration_spec.rb
51
+
52
+ RSpec/MultipleMemoizedHelpers:
53
+ Exclude:
54
+ - spec/go_puff/prodcat_api/memo_spec.rb
55
+ - spec/go_puff/prodcat_api/fetch_products_spec.rb
56
+
57
+ Metrics/CyclomaticComplexity:
58
+ Exclude:
59
+ - lib/go_puff/prodcat_api/fetch_products.rb
60
+
61
+ RSpec/NestedGroups:
62
+ Exclude:
63
+ - spec/go_puff/prodcat_api/structs_spec.rb
64
+ - spec/go_puff/prodcat_api/type_utility_spec.rb
data/Dockerfile ADDED
@@ -0,0 +1,22 @@
1
+ FROM ruby:2.7-slim
2
+
3
+ RUN apt-get update \
4
+ && apt-get install -y --no-install-recommends \
5
+ bash less git \
6
+ build-essential ruby-dev \
7
+ && apt-get clean
8
+
9
+ ENV APP_HOME /app
10
+
11
+ ENV BUNDLE_GEMFILE=$APP_HOME/Gemfile \
12
+ BUNDLE_JOBS=8 \
13
+ BUNDLE_PATH=/gems
14
+
15
+ RUN mkdir $APP_HOME
16
+
17
+ WORKDIR $APP_HOME
18
+
19
+ ADD Gemfile $APP_HOME/Gemfile
20
+ ADD Gemfile.lock $APP_HOME/Gemfile.lock
21
+
22
+ RUN gem install bundler --no-document
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+ source 'https://rubygems.pkg.github.com/gopuff'
5
+ # Specify your gem's dependencies in go_puff-prodcat_api.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,112 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ go_puff-prodcat_api (0.9.3.pre.beta.1)
5
+ go_puff-http-client (~> 0.5)
6
+ retryable
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ remote: https://rubygems.pkg.github.com/gopuff/
11
+ specs:
12
+ addressable (2.8.0)
13
+ public_suffix (>= 2.0.2, < 5.0)
14
+ ast (2.4.2)
15
+ coderay (1.1.3)
16
+ connection_pool (2.3.0)
17
+ crack (0.4.5)
18
+ rexml
19
+ diff-lcs (1.5.0)
20
+ docile (1.4.0)
21
+ faraday (2.7.4)
22
+ faraday-net_http (>= 2.0, < 3.1)
23
+ ruby2_keywords (>= 0.0.4)
24
+ faraday-net_http (3.0.2)
25
+ faraday-net_http_persistent (2.1.0)
26
+ faraday (~> 2.5)
27
+ net-http-persistent (~> 4.0)
28
+ go_puff-http-client (0.5.1)
29
+ faraday (~> 2.5)
30
+ faraday-net_http_persistent (~> 2.1)
31
+ hashdiff (1.0.1)
32
+ method_source (1.0.0)
33
+ net-http-persistent (4.0.1)
34
+ connection_pool (~> 2.2)
35
+ parallel (1.22.1)
36
+ parser (3.1.2.0)
37
+ ast (~> 2.4.1)
38
+ pry (0.14.1)
39
+ coderay (~> 1.1)
40
+ method_source (~> 1.0)
41
+ public_suffix (4.0.7)
42
+ rainbow (3.1.1)
43
+ regexp_parser (2.4.0)
44
+ retryable (3.0.5)
45
+ rexml (3.2.5)
46
+ rspec (3.11.0)
47
+ rspec-core (~> 3.11.0)
48
+ rspec-expectations (~> 3.11.0)
49
+ rspec-mocks (~> 3.11.0)
50
+ rspec-core (3.11.0)
51
+ rspec-support (~> 3.11.0)
52
+ rspec-expectations (3.11.0)
53
+ diff-lcs (>= 1.2.0, < 2.0)
54
+ rspec-support (~> 3.11.0)
55
+ rspec-mocks (3.11.1)
56
+ diff-lcs (>= 1.2.0, < 2.0)
57
+ rspec-support (~> 3.11.0)
58
+ rspec-support (3.11.0)
59
+ rubocop (1.29.1)
60
+ parallel (~> 1.10)
61
+ parser (>= 3.1.0.0)
62
+ rainbow (>= 2.2.2, < 4.0)
63
+ regexp_parser (>= 1.8, < 3.0)
64
+ rexml (>= 3.2.5, < 4.0)
65
+ rubocop-ast (>= 1.17.0, < 2.0)
66
+ ruby-progressbar (~> 1.7)
67
+ unicode-display_width (>= 1.4.0, < 3.0)
68
+ rubocop-ast (1.17.0)
69
+ parser (>= 3.1.1.0)
70
+ rubocop-performance (1.13.3)
71
+ rubocop (>= 1.7.0, < 2.0)
72
+ rubocop-ast (>= 0.4.0)
73
+ rubocop-rspec (2.10.0)
74
+ rubocop (~> 1.19)
75
+ rubocop-thread_safety (0.4.4)
76
+ rubocop (>= 0.53.0)
77
+ ruby-progressbar (1.11.0)
78
+ ruby2_keywords (0.0.5)
79
+ simplecov (0.21.2)
80
+ docile (~> 1.1)
81
+ simplecov-html (~> 0.11)
82
+ simplecov_json_formatter (~> 0.1)
83
+ simplecov-html (0.12.3)
84
+ simplecov-lcov (0.8.0)
85
+ simplecov_json_formatter (0.1.4)
86
+ unicode-display_width (2.1.0)
87
+ webmock (3.14.0)
88
+ addressable (>= 2.8.0)
89
+ crack (>= 0.3.2)
90
+ hashdiff (>= 0.4.0, < 2.0.0)
91
+
92
+ PLATFORMS
93
+ aarch64-linux
94
+ x86_64-darwin-18
95
+ x86_64-darwin-22
96
+ x86_64-linux
97
+
98
+ DEPENDENCIES
99
+ bundler (~> 2.0)
100
+ go_puff-prodcat_api!
101
+ pry (~> 0.14)
102
+ rspec (~> 3.0)
103
+ rubocop (~> 1.25)
104
+ rubocop-performance
105
+ rubocop-rspec
106
+ rubocop-thread_safety (~> 0.4)
107
+ simplecov (~> 0.21)
108
+ simplecov-lcov (~> 0.8.0)
109
+ webmock (~> 3.5)
110
+
111
+ BUNDLED WITH
112
+ 2.3.22
data/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # GoPuff::ProdcatApi
2
+ [![Coverage Status](https://coveralls.io/repos/github/gopuff/go_puff-prodcat_api/badge.svg?branch=main&t=K8IagK)](https://coveralls.io/github/gopuff/go_puff-prodcat_api?branch=main)
3
+
4
+ This Rails library is designed to work with product-catalog/pds api
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ source 'https://rubygems.pkg.github.com/gopuff' do
12
+ gem 'go_puff-prodcat_api'
13
+ end
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ $ bundle install
19
+
20
+ ## Set up
21
+ ```ruby
22
+ # config/initializers/prodcat_api.rb
23
+
24
+ GoPuff::ProdcatApi.configure do |c|
25
+ c.logger = GoPuff::Logger
26
+ c.url = ENV.fetch('PDS_API_URL')
27
+ c.authorization_client_id = ENV.fetch('PRODCAT_API_AUTHORIZATION_CLIENT_ID')
28
+ c.user_agent_header = GoPuff::Http.config.user_agent_header
29
+
30
+ # config.requests.max_retries = 3 # default
31
+ # config.requests.retry_wait_time = 0.5 # default
32
+ # config.requests.retryable_exceptions = [EOFError, Errno::ECONNRESET, Timeout::Error, GoPuff::ProdcatApi::ServerError, GoPuff::ProdcatApi::ClientError, GoPuff::ProdcatApi::TimeoutError] # default
33
+ end
34
+ ```
35
+
36
+ ## Local development with docker
37
+ Authorize personal access token for Github by following the steps at https://docs.github.com/en/github/authenticating-to-github/authorizing-a-personal-access-token-for-use-with-saml-single-sign-on
38
+
39
+
40
+ ```bash
41
+ cp .bundle/config.example .bundle/config
42
+ ```
43
+ Add Github username and personal access token in `./bundle/config` and start project using:
44
+
45
+ ```bash
46
+ docker-compose run --rm go_puff-prodcat_api bash
47
+ ```
48
+ or with [dip](https://github.com/bibendi/dip#installation)
49
+
50
+ ```bash
51
+ dip bash
52
+ ```
53
+
54
+ ## Contributing
55
+
56
+ Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**.
57
+
58
+ 1. Clone the Project
59
+ 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
60
+ 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
61
+ 4. Push to the Branch (`git push origin feature/AmazingFeature`)
62
+ 5. Open a Pull Request
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'go_puff/prodcat_api'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
data/bin/rspec ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require 'pathname'
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path('bundle', __dir__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if /This file was generated by Bundler/.match?(File.read(bundle_binstub, 300))
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require 'rubygems'
27
+ require 'bundler/setup'
28
+
29
+ load Gem.bin_path('rspec-core', 'rspec')
data/bin/rubocop ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require 'pathname'
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path('bundle', __dir__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if /This file was generated by Bundler/.match?(File.read(bundle_binstub, 300))
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require 'rubygems'
27
+ require 'bundler/setup'
28
+
29
+ load Gem.bin_path('rubocop', 'rubocop')
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/dip.yml ADDED
@@ -0,0 +1,23 @@
1
+ version: '2'
2
+
3
+ environment:
4
+ RAILS_ENV: development
5
+
6
+ compose:
7
+ files:
8
+ - docker-compose.yml
9
+
10
+ interaction:
11
+ bash:
12
+ service: go_puff-prodcat_api
13
+ command: /bin/bash
14
+ compose_run_options: [no-deps]
15
+ bundle:
16
+ service: go_puff-prodcat_api
17
+ command: bundle
18
+ rake:
19
+ service: go_puff-prodcat_api
20
+ command: bundle exec rake
21
+ rspec:
22
+ service: go_puff-prodcat_api
23
+ command: bundle exec rspec
@@ -0,0 +1,12 @@
1
+ version: '3.4'
2
+
3
+ services:
4
+ go_puff-prodcat_api:
5
+ build:
6
+ context: .
7
+ volumes:
8
+ - .:/app
9
+ - go_puff-prodcat_api-gems:/gems
10
+ - ./.bundle/config:/root/.bundle/config
11
+ volumes:
12
+ go_puff-prodcat_api-gems:
data/lefthook.yml ADDED
@@ -0,0 +1,6 @@
1
+ pre-commit:
2
+ commands:
3
+ rubocop:
4
+ files: git diff --name-only --staged
5
+ glob: "*.rb"
6
+ run: rubocop --force-exclusion {files}
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GoPuff
4
+ module ProdcatApi
5
+ class Configuration
6
+ attr_accessor :url, :authorization_client_id, :requests, :logger, :user_agent_header
7
+
8
+ def initialize
9
+ @requests = Struct.new(:max_retries, :retry_wait_time, :retryable_exceptions) do
10
+ def to_h
11
+ {
12
+ on: retryable_exceptions,
13
+ tries: max_retries,
14
+ sleep: retry_wait_time
15
+ }
16
+ end
17
+ end.new(3, 0.5, [GoPuff::ProdcatApi::InvalidJsonResponseError, GoPuff::ProdcatApi::HttpError])
18
+ end
19
+
20
+ def validate!
21
+ raise ConfigError, 'Undefined url configuration in config/initializers/prodcat_api.rb' if url.nil?
22
+ raise ConfigError, 'Undefined authorization_client_id configuration in config/initializers/prodcat_api.rb' if authorization_client_id.nil?
23
+ raise ConfigError, 'Undefined logger configuration in config/initializers/prodcat_api.rb' if logger.nil?
24
+ raise ConfigError, 'Undefined user_agent_header configuration in config/initializers/go_puff-http-client.rb' if user_agent_header.nil?
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GoPuff
4
+ module ProdcatApi
5
+ class Error < StandardError
6
+ end
7
+
8
+ class HttpError < Error
9
+ attr_reader :original
10
+
11
+ def initialize(msg, original = $ERROR_INFO)
12
+ super(msg)
13
+ @original = original
14
+ end
15
+ end
16
+
17
+ class ConfigError < Error; end
18
+
19
+ class InvalidJsonResponseError < Error
20
+ def initialize(msg, code, body = nil)
21
+ msg += " | code: #{code}"
22
+ msg += " | body: #{body}" if body
23
+ super(msg)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GoPuff
4
+ module ProdcatApi
5
+ class FetchProduct < FetchProducts
6
+ def initialize(product_id:, location_id:, point_of_sale: 'US', headers: {}, thread: nil)
7
+ super(
8
+ product_ids: [product_id],
9
+ location_id: location_id,
10
+ point_of_sale: point_of_sale,
11
+ headers: headers,
12
+ thread: thread,
13
+ )
14
+ end
15
+
16
+ def call
17
+ super&.first
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './errors'
4
+ require_relative './memo'
5
+ require_relative './structs'
6
+
7
+ module GoPuff
8
+ module ProdcatApi
9
+ class FetchProducts
10
+ include Structs
11
+
12
+ attr_reader :time, :memo, :thread
13
+
14
+ TIMEOUT = 5
15
+
16
+ def initialize(product_ids:, location_id:, point_of_sale: 'US', headers: {}, thread: nil)
17
+ @location_id = location_id
18
+ @product_ids = product_ids.compact
19
+ @point_of_sale = point_of_sale
20
+ @headers = headers
21
+ @thread = thread
22
+
23
+ @memo = Memo.new(product_ids, location_id, point_of_sale, thread) if thread && web?
24
+ end
25
+
26
+ def self.call(*args, **kwargs, &block)
27
+ new(*args, **kwargs, &block).call
28
+ end
29
+
30
+ def call
31
+ return [] if @product_ids.empty? || @location_id.nil?
32
+
33
+ return memo.products if web? && memo&.exist?
34
+
35
+ Retryable.retryable(GoPuff::ProdcatApi.config.requests.to_h) do
36
+ make_request
37
+ end
38
+
39
+ products = parse_response
40
+
41
+ memo&.add(products) if web?
42
+
43
+ products
44
+ end
45
+
46
+ private
47
+
48
+ def make_request
49
+ @response = GoPuff::Http::Client.new(uri.to_s, adapter: :net_http_persistent, timeout: TIMEOUT).get(headers: headers, json: false)
50
+ raise(HttpError, "#{@response.error_message} | #{@response.status}") unless @response.success?
51
+
52
+ @response_json = JSON.parse(@response.body)
53
+ rescue JSON::ParserError => _e
54
+ raise InvalidJsonResponseError.new(uri.to_s, parse_code_from_body, @response.body)
55
+ rescue StandardError => e
56
+ raise HttpError, e
57
+ end
58
+
59
+ def web?
60
+ return false unless defined?(Sidekiq)
61
+
62
+ !Sidekiq.server?.nil?
63
+ end
64
+
65
+ def uri
66
+ @uri ||= begin
67
+ uri = URI("#{url}/api/products")
68
+ uri.query = "product_ids=#{@product_ids.join(',')}&location_id=#{@location_id}&page_size=#{@product_ids.size}"
69
+ uri.query += '&datasource=eventing' if Flipper.enabled?(:puff_77780_change_split_for_bundles)
70
+ uri
71
+ end
72
+ end
73
+
74
+ def url
75
+ GoPuff::ProdcatApi.config.url
76
+ end
77
+
78
+ def parse_code_from_body
79
+ code = @response.body[/\d+/].to_i
80
+ return 500 if code.zero?
81
+
82
+ code
83
+ end
84
+
85
+ def parse_response
86
+ @prodcat_products = @response_json['products'].map do |product|
87
+ ProductStruct.new.tap do |struct|
88
+ struct.product_id = product['product_id']
89
+ struct.price = product['price']
90
+ struct.offer = product['offer']
91
+ struct.cost = product['cost']
92
+ struct.kind = kind(product)
93
+ struct.is_rx = product['is_rx']
94
+ struct.is_sip = product['is_sip']
95
+ struct.is_digital = product['is_digital']
96
+ struct.currency = product['currency']
97
+ struct.subcategory_id = product['subcategory_id']
98
+ struct.primary_subcategory_id = product['primary_subcategory_id']
99
+ struct.quantity = product['quantity']
100
+ struct.available_for_bonus = product['available_for_bonus']
101
+ struct.bundle = product['bundle']
102
+ struct.is_sample = TypeUtility.to_boolean(product['is_sample'])
103
+ struct.classifications = product['classifications'] || []
104
+
105
+ struct.bundle_products = product['bundleProducts']&.map do |bundle_product|
106
+ BundleProductStruct.new.tap do |bp_struct|
107
+ bp_struct.product_id = bundle_product['product_id'].to_i
108
+ bp_struct.quantity = bundle_product['quantity']
109
+ bp_struct.cost = bundle_product['cost']
110
+ end
111
+ end
112
+ struct.images = product['images']&.map do |product_image|
113
+ ImageStruct.new.tap do |img_struct|
114
+ img_struct.small = product_image['small']
115
+ img_struct.thumb = product_image['thumb']
116
+ img_struct.medium = product_image['medium']
117
+ img_struct.xlarge = product_image['xlarge']
118
+ img_struct.tile_image_alt_text = product_image['tile_image_alt_text']
119
+ end
120
+ end
121
+ struct.avatar = AvatarStruct.new.tap do |avatar_struct|
122
+ avatar_struct.small = product.dig('avatar', 'small')
123
+ avatar_struct.thumb = product.dig('avatar', 'thumb')
124
+ avatar_struct.medium = product.dig('avatar', 'medium')
125
+ avatar_struct.xlarge = product.dig('avatar', 'xlarge')
126
+ avatar_struct.tile_image_alt_text = product.dig('avatar', 'tile_image_alt_text')
127
+ end
128
+ struct.upc_barcodes = product['upc']
129
+ GoPuff::ProdcatApi.config.logger.info('Prodcat api product item',
130
+ log_parent_key: :prodcat_api_product_item,
131
+ params: { **struct.to_h, point_of_sale: @point_of_sale })
132
+ end
133
+ end
134
+ end
135
+
136
+ def headers
137
+ {
138
+ 'Content-Type' => 'application/json',
139
+ 'Authorization-Client-ID' => GoPuff::ProdcatApi.config.authorization_client_id,
140
+ 'X-GP-Point-Of-Sale' => @point_of_sale,
141
+ 'X-AMZN-TRACE-ID' => @headers['X-AMZN-TRACEID'],
142
+ 'X-APPGW-TRACE-ID' => @headers['X-APPGW_TRACE-ID'],
143
+ 'X-MS-REQUEST-ID' => @headers['X-MS-REQUEST-ID'],
144
+ 'X-MS-REQUEST-ROOT-ID' => @headers['X-MS-REQUEST-ROOT-ID'],
145
+ 'REQUEST-ID' => @headers['REQUEST-ID'],
146
+ 'TRACEPARENT' => @headers['TRACEPARENT']
147
+ }.compact
148
+ end
149
+
150
+ def kind(product)
151
+ return product['kind'] if product['bundle_info'].nil? || product['bundle_info'].empty?
152
+ return 'alcohol' if product['bundle_info']['products'].map { |item| item['kind'] }.include? 'alcohol'
153
+
154
+ product['bundle_info']['products'][0]['kind']
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GoPuff
4
+ module ProdcatApi
5
+ class Memo
6
+ CACHE_NAMESPACE = :pds_api_product_cache
7
+
8
+ attr_reader :product_ids, :location_id, :point_of_sale, :thread
9
+
10
+ def initialize(product_ids, location_id, point_of_sale, thread)
11
+ @product_ids = product_ids
12
+ @location_id = location_id
13
+ @point_of_sale = point_of_sale
14
+ @thread = thread
15
+ end
16
+
17
+ def products
18
+ cache = thread[CACHE_NAMESPACE]
19
+
20
+ return if cache.nil?
21
+
22
+ product_ids.lazy.uniq.map { |id| product_key(id) }.map { |key| cache[key] }.to_a
23
+ end
24
+
25
+ def exist?
26
+ cache = thread[CACHE_NAMESPACE]
27
+
28
+ return false if cache.nil?
29
+
30
+ product_ids.lazy.map { |id| product_key(id) }.all? { |key| cache[key] }
31
+ end
32
+
33
+ def add(products)
34
+ thread[CACHE_NAMESPACE] ||= {}
35
+ cache = thread[CACHE_NAMESPACE]
36
+
37
+ products.each do |product|
38
+ key = product_key(product.product_id)
39
+
40
+ cache[key] = product
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def product_key(product_id)
47
+ "#{point_of_sale}_#{location_id}_#{product_id}"
48
+ end
49
+
50
+ class << self
51
+ def delete
52
+ Thread.current[CACHE_NAMESPACE] = nil
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GoPuff
4
+ module ProdcatApi
5
+ module ProductHydration
6
+ class LookUpByBarcode
7
+ include Structs
8
+
9
+ TIMEOUT = 5
10
+
11
+ def initialize(barcode, point_of_sale: 'US')
12
+ @barcode = barcode
13
+ @point_of_sale = point_of_sale
14
+ end
15
+
16
+ def self.call(*args, **kwargs, &block)
17
+ new(*args, **kwargs, &block).call
18
+ end
19
+
20
+ def call
21
+ return [] if @barcode.nil?
22
+
23
+ Retryable.retryable(GoPuff::ProdcatApi.config.requests.to_h) do
24
+ make_request
25
+ end
26
+
27
+ parse_response
28
+ end
29
+
30
+ private
31
+
32
+ def make_request
33
+ @response = GoPuff::Http::Client.new(uri.to_s, adapter: :net_http_persistent, timeout: TIMEOUT).get(headers: headers, json: false)
34
+
35
+ raise(HttpError, "#{@response.error_message} | #{@response.status}") unless @response.success?
36
+
37
+ @response_json = JSON.parse(@response.body)
38
+ rescue JSON::ParserError => _e
39
+ raise InvalidJsonResponseError.new(uri.to_s, parse_code_from_body, @response.body)
40
+ rescue StandardError => e
41
+ raise HttpError, e
42
+ end
43
+
44
+ def uri
45
+ @uri ||= begin
46
+ url_safe_barcode = ERB::Util.url_encode(@barcode.strip)
47
+ uri = URI("#{url}/product-hydration/upc/#{url_safe_barcode}")
48
+ uri.query = 'datasource=cosmos'
49
+
50
+ uri
51
+ end
52
+ end
53
+
54
+ def url
55
+ GoPuff::ProdcatApi.config.url
56
+ end
57
+
58
+ def parse_code_from_body
59
+ code = @response.body[/\d+/].to_i
60
+ return 500 if code.zero?
61
+
62
+ code
63
+ end
64
+
65
+ def parse_response
66
+ @prodcat_products = @response_json['products'].map do |product|
67
+ product_mapper(product)
68
+ end
69
+ end
70
+
71
+ def headers
72
+ {
73
+ 'Content-Type' => 'application/json',
74
+ 'Authorization-Client-ID' => GoPuff::ProdcatApi.config.authorization_client_id,
75
+ 'X-GP-Point-Of-Sale' => @point_of_sale
76
+ }.compact
77
+ end
78
+
79
+ def product_mapper(product)
80
+ ProductStruct.new.tap do |struct|
81
+ struct.product_id = product['id']
82
+ struct.price = product['price']
83
+ struct.offer = product['offer']
84
+ struct.cost = product['cost']
85
+ struct.kind = product['kind']
86
+ struct.currency = product['currency']
87
+ struct.subcategory_id = product['subcategoryId']
88
+ struct.quantity = product['quantity']
89
+ struct.available_for_bonus = product['availableForBonus']
90
+ struct.bundle = product['bundle']
91
+ struct.upc_barcodes = product['upcs']
92
+
93
+ # structure mappers
94
+ struct.bundle_products = product.dig('bundleInfo', 'products')&.map(&method(:bundle_mapper))
95
+ struct.images = product['imagesOld']&.map(&method(:image_mapper))
96
+ struct.avatar = avatar_mapper(product['avatarOld'].to_h)
97
+ end
98
+ end
99
+
100
+ def bundle_mapper(bundle_product)
101
+ BundleProductStruct.new.tap do |bundle_struct|
102
+ bundle_struct.product_id = bundle_product['id'].to_i
103
+ bundle_struct.quantity = bundle_product['quantity']
104
+ bundle_struct.cost = bundle_product['price']
105
+ end
106
+ end
107
+
108
+ def image_mapper(product_image)
109
+ ImageStruct.new.tap do |image_struct|
110
+ image_struct.small = product_image['small']
111
+ image_struct.thumb = product_image['thumb']
112
+ image_struct.medium = product_image['medium']
113
+ image_struct.large = product_image['large']
114
+ image_struct.xlarge = product_image['xlarge']
115
+ image_struct.tile_image_alt_text = product_image['tile_image_alt_text']
116
+ end
117
+ end
118
+
119
+ def avatar_mapper(product_avatar)
120
+ ImageStruct.new.tap do |image_struct|
121
+ image_struct.small = product_avatar['small']
122
+ image_struct.thumb = product_avatar['thumb']
123
+ image_struct.medium = product_avatar['medium']
124
+ image_struct.large = product_avatar['large']
125
+ image_struct.xlarge = product_avatar['xlarge']
126
+ image_struct.tile_image_alt_text = product_avatar['tile_image_alt_text']
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GoPuff
4
+ module ProdcatApi
5
+ module Structs
6
+ PRODUCT_STRUCT_PARAMS = %i[
7
+ product_id
8
+ price
9
+ quantity
10
+ currency
11
+ offer
12
+ bundle
13
+ cost
14
+ available_for_bonus
15
+ bundle_products
16
+ kind
17
+ primary_subcategory_id
18
+ subcategory_id
19
+ is_sample
20
+ is_rx
21
+ is_sip
22
+ is_digital
23
+ classifications
24
+ images
25
+ avatar
26
+ upc_barcodes
27
+ ].freeze
28
+
29
+ ProductStruct = Struct.new(*PRODUCT_STRUCT_PARAMS) do
30
+ def to_h
31
+ super.tap do |ps|
32
+ ps[:bundle_products] = ps[:bundle_products]&.map(&:to_h)
33
+ end
34
+ end
35
+
36
+ def rx?
37
+ TypeUtility.to_boolean(is_rx)
38
+ end
39
+
40
+ def sip?
41
+ TypeUtility.to_boolean(is_sip)
42
+ end
43
+
44
+ def digital?
45
+ TypeUtility.to_boolean(is_digital)
46
+ end
47
+
48
+ def sample?
49
+ TypeUtility.to_boolean(is_sample)
50
+ end
51
+ end
52
+
53
+ BundleProductStruct = Struct.new(:product_id, :quantity, :cost)
54
+ ImageStruct = Struct.new(:small, :thumb, :medium, :large, :xlarge, :tile_image_alt_text)
55
+ AvatarStruct = Struct.new(:small, :thumb, :medium, :large, :xlarge, :tile_image_alt_text)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GoPuff
4
+ module ProdcatApi
5
+ module TypeUtility
6
+ def self.to_boolean(value)
7
+ %w[1 true yes t y].include?(value&.to_s&.downcase)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GoPuff
4
+ module ProdcatApi
5
+ VERSION = '0.9.3.pre.beta.1'
6
+ end
7
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'retryable'
4
+ require 'benchmark'
5
+ require 'go_puff/http/client'
6
+ require_relative 'prodcat_api/version'
7
+ require_relative 'prodcat_api/errors'
8
+ require_relative 'prodcat_api/configuration'
9
+ require_relative 'prodcat_api/fetch_products'
10
+ require_relative 'prodcat_api/fetch_product'
11
+ require_relative 'prodcat_api/structs'
12
+ require_relative 'prodcat_api/type_utility'
13
+ require_relative 'prodcat_api/product_hydration/look_up_by_barcode'
14
+
15
+ module GoPuff
16
+ module ProdcatApi
17
+ class << self
18
+ attr_accessor :config
19
+
20
+ def configure
21
+ @config ||= Configuration.new
22
+
23
+ yield config if block_given?
24
+ config.validate!
25
+ GoPuff::Http.config.logger = config.logger
26
+ GoPuff::Http.config.user_agent_header = config.user_agent_header
27
+ config
28
+ end
29
+ end
30
+ end
31
+ end
metadata ADDED
@@ -0,0 +1,236 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: go_puff-prodcat_api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.3.pre.beta.1
5
+ platform: ruby
6
+ authors:
7
+ - Ruslan Tolstov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-02-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: go_puff-http-client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: retryable
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.14'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.14'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.25'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.25'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop-performance
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop-rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop-thread_safety
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.4'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.4'
139
+ - !ruby/object:Gem::Dependency
140
+ name: simplecov
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.21'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '0.21'
153
+ - !ruby/object:Gem::Dependency
154
+ name: simplecov-lcov
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: 0.8.0
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: 0.8.0
167
+ - !ruby/object:Gem::Dependency
168
+ name: webmock
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '3.5'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '3.5'
181
+ description:
182
+ email:
183
+ executables: []
184
+ extensions: []
185
+ extra_rdoc_files: []
186
+ files:
187
+ - ".bundle/config.example"
188
+ - ".rspec"
189
+ - ".rubocop.yml"
190
+ - Dockerfile
191
+ - Gemfile
192
+ - Gemfile.lock
193
+ - README.md
194
+ - Rakefile
195
+ - bin/console
196
+ - bin/rspec
197
+ - bin/rubocop
198
+ - bin/setup
199
+ - dip.yml
200
+ - docker-compose.yml
201
+ - lefthook.yml
202
+ - lib/go_puff/prodcat_api.rb
203
+ - lib/go_puff/prodcat_api/configuration.rb
204
+ - lib/go_puff/prodcat_api/errors.rb
205
+ - lib/go_puff/prodcat_api/fetch_product.rb
206
+ - lib/go_puff/prodcat_api/fetch_products.rb
207
+ - lib/go_puff/prodcat_api/memo.rb
208
+ - lib/go_puff/prodcat_api/product_hydration/look_up_by_barcode.rb
209
+ - lib/go_puff/prodcat_api/structs.rb
210
+ - lib/go_puff/prodcat_api/type_utility.rb
211
+ - lib/go_puff/prodcat_api/version.rb
212
+ homepage: https://github.com/gopuff/go_puff-prodcat_api
213
+ licenses:
214
+ - ISC
215
+ metadata:
216
+ rubygems_mfa_required: 'true'
217
+ post_install_message:
218
+ rdoc_options: []
219
+ require_paths:
220
+ - lib
221
+ required_ruby_version: !ruby/object:Gem::Requirement
222
+ requirements:
223
+ - - ">="
224
+ - !ruby/object:Gem::Version
225
+ version: 2.6.0
226
+ required_rubygems_version: !ruby/object:Gem::Requirement
227
+ requirements:
228
+ - - ">"
229
+ - !ruby/object:Gem::Version
230
+ version: 1.3.1
231
+ requirements: []
232
+ rubygems_version: 3.1.4
233
+ signing_key:
234
+ specification_version: 4
235
+ summary: This Rails library is designed to work with product-catalog/pds api
236
+ test_files: []