go_puff-prodcat_api 0.9.3.pre.beta.1

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.
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: []