diff --git a/lib/bls/core.rb b/lib/bls/core.rb index f672ea4..4a3a3db 100644 --- a/lib/bls/core.rb +++ b/lib/bls/core.rb @@ -7,7 +7,9 @@ require_relative "core/api_client" require_relative "core/api/v1" +require_relative "core/api/v2" require_relative "core/api/v1/authenticator" +require_relative "core/api/v2/authenticator" require_relative "core/api_operations/retrieve" require_relative "core/authorization/base" require_relative "core/authorization/basic" @@ -15,13 +17,19 @@ require_relative "core/resources/api_error" require_relative "core/authorization/bearer" require_relative "core/resources/object" +require_relative "core/resources/objectv2" +require_relative "core/resources/allergen" +require_relative "core/resources/dietary_tag" +require_relative "core/resources/ingredient" require_relative "core/resources/inventory_level" +require_relative "core/resources/nutrition_fact" require_relative "core/resources/order" require_relative "core/resources/order_item" require_relative "core/serializers/order_item_serializer" require_relative "core/resources/menu" require_relative "core/resources/menu_item" require_relative "core/serializers/menu_item_serializer" +require_relative "core/resources/product" require_relative "core/resources/recipient" require_relative "core/serializers/recipient_serializer" require_relative "core/version" @@ -32,7 +40,8 @@ module Core class Error < StandardError; end class << self - attr_accessor :client_id, :client_secret, :environment + attr_accessor :client_id, :client_secret, :environment, :username, + :password def configure yield self diff --git a/lib/bls/core/api/v1/authenticator.rb b/lib/bls/core/api/v1/authenticator.rb index 77c9e17..6490b18 100644 --- a/lib/bls/core/api/v1/authenticator.rb +++ b/lib/bls/core/api/v1/authenticator.rb @@ -15,7 +15,7 @@ def save! path = "#{base_url}/token" body = { client_id: client_id, client_secret: client_secret } auth = Authorization::Basic. - factory(client_id: client_id, client_secret: client_secret) + factory(username: client_id, password: client_secret) client = Core::ApiClient.new(authentication: auth) response = client.post(path: path, body: body) handle_authentication_response!(response: response.data) diff --git a/lib/bls/core/api/v2.rb b/lib/bls/core/api/v2.rb new file mode 100644 index 0000000..3f6dd40 --- /dev/null +++ b/lib/bls/core/api/v2.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module Bls + module Core + class V2 + attr_accessor :authentication + attr_reader :environment, :username, :password + + PRODUCTION = "production" + STAGING = "staging" + + def initialize(username: nil, password: nil, authentication: nil, + environment: STAGING) + @authentication = authentication + @environment = environment + @username = username + @password = password + end + + def base_url + "https://core.batchlinesolutions.#{tld}" + end + + def authenticate! + return if authenticated? + + authenticator = V2::Authenticator.build(self) + @authentication = authenticator.save! + end + + def client + build_api_client! + end + + def authenticated? + authentication.present? + end + + def reset_authentication + @authentication = nil + self + end + + def self.build(config: Bls::Core) + new(environment: config.environment, + username: config.username, + password: config.password) + end + + protected + + def build_api_client! + authenticate! + Bls::Core::ApiClient.build(self) + end + + def tld + @tld ||= determine_tld + end + + def determine_tld + if environment == PRODUCTION + "com" + else + "dev" + end + end + end + end +end diff --git a/lib/bls/core/api/v2/authenticator.rb b/lib/bls/core/api/v2/authenticator.rb new file mode 100644 index 0000000..ea2c316 --- /dev/null +++ b/lib/bls/core/api/v2/authenticator.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Bls + module Core + class V2::Authenticator + attr_accessor :authentication + + def initialize(authentication:) + @authentication = authentication + end + + def save! + authentication + end + + def self.build(api) + authentication = Authorization::Basic. + factory(username: api.username, + password: api.password) + new(authentication: authentication) + end + end + end +end diff --git a/lib/bls/core/authorization/basic.rb b/lib/bls/core/authorization/basic.rb index 010c8ea..dd6d422 100644 --- a/lib/bls/core/authorization/basic.rb +++ b/lib/bls/core/authorization/basic.rb @@ -8,8 +8,8 @@ def header "Basic #{token}" end - def self.factory(client_id:, client_secret:) - encoded_token = Base64.strict_encode64("#{client_id}:#{client_secret}") + def self.factory(username:, password:) + encoded_token = Base64.strict_encode64("#{username}:#{password}") new(token: encoded_token) end end diff --git a/lib/bls/core/resources/allergen.rb b/lib/bls/core/resources/allergen.rb new file mode 100644 index 0000000..6601376 --- /dev/null +++ b/lib/bls/core/resources/allergen.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Bls + module Core + class Allergen < Bls::Core::ObjectV2; end + end +end diff --git a/lib/bls/core/resources/api_error.rb b/lib/bls/core/resources/api_error.rb index 1a7475f..07ba116 100644 --- a/lib/bls/core/resources/api_error.rb +++ b/lib/bls/core/resources/api_error.rb @@ -22,7 +22,9 @@ def message def self.build(response) data = response.data - data => { status:, title:, details: } + status = data.fetch(:status, data[:type]) + title = data.fetch(:title, data[:message]) + details = data[:details] new(status: status, title: title, details: details) end diff --git a/lib/bls/core/resources/dietary_tag.rb b/lib/bls/core/resources/dietary_tag.rb new file mode 100644 index 0000000..db080fd --- /dev/null +++ b/lib/bls/core/resources/dietary_tag.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Bls + module Core + class DietaryTag < Bls::Core::ObjectV2; end + end +end diff --git a/lib/bls/core/resources/ingredient.rb b/lib/bls/core/resources/ingredient.rb new file mode 100644 index 0000000..c82fbc5 --- /dev/null +++ b/lib/bls/core/resources/ingredient.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Bls + module Core + class Ingredient < Bls::Core::ObjectV2; end + end +end diff --git a/lib/bls/core/resources/nutrition_fact.rb b/lib/bls/core/resources/nutrition_fact.rb new file mode 100644 index 0000000..77e3de7 --- /dev/null +++ b/lib/bls/core/resources/nutrition_fact.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Bls + module Core + class NutritionFact < Bls::Core::ObjectV2; end + end +end diff --git a/lib/bls/core/resources/objectv2.rb b/lib/bls/core/resources/objectv2.rb new file mode 100644 index 0000000..9e3c447 --- /dev/null +++ b/lib/bls/core/resources/objectv2.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Bls + module Core + class ObjectV2 < Bls::Core::Object + def self.build_from_array(array) + array.map { |object| build(object) } + end + + def resources_path + "#{api.base_url}/api/#{object_name.downcase}" + end + + def resource_path + "#{api.base_url}/api/#{object_name.downcase}/#{id}" + end + + def api + @api ||= Bls::Core::V2.build + end + end + end +end diff --git a/lib/bls/core/resources/product.rb b/lib/bls/core/resources/product.rb new file mode 100644 index 0000000..e3725eb --- /dev/null +++ b/lib/bls/core/resources/product.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Bls + module Core + class Product < Bls::Core::ObjectV2 + def initialize(args = {}) + super + end + + def object_name + "products" + end + + def self.build_from_response(data) + product = build(data) + product.ingredients = Ingredient.build_from_array(data[:ingredients]) + as_packaged = NutritionFact. + build_from_array(data[:nutrition_facts][:as_packaged]) + as_cooked = NutritionFact. + build_from_array(data[:nutrition_facts]. + fetch(:as_cooked, [])) + product.allergens = Allergen.build_from_array(data[:allergens]) + product.dietary_tags = DietaryTag.build_from_array(data[:dietary_tags]) + product.nutrition_facts = { as_packaged: as_packaged, + as_cooked: as_cooked } + product + end + end + end +end diff --git a/lib/bls/core/testing/fake.rb b/lib/bls/core/testing/fake.rb index 5c03dd3..2f2d05b 100644 --- a/lib/bls/core/testing/fake.rb +++ b/lib/bls/core/testing/fake.rb @@ -6,6 +6,12 @@ module Testing class Fake def self.configure(*); end + class Allergen + def self.new(code: "MILK", source: "ANCHOVY") + OpenStruct.new(code: code, source: source) + end + end + class DistributionCenterInventory def self.create(name: "WEST_COAST", quantity: 100) OpenStruct.new(distribution_center: name, quantity: quantity) @@ -90,6 +96,15 @@ def self.new(sku:, quantity:, protein_sku:) end end + class Product + def self.new(code:, name:, status: "DRAFT", nutrition_facts: []) + OpenStruct.new(code: code, name: name, status: status, + nutrition_facts: nutrition_facts, + allergens: [Fake::Allergen.new], + dietary_tags: ["NUTS"]) + end + end + def self.client Fake::Client.new end diff --git a/spec/core/api/v2/authenticator_spec.rb b/spec/core/api/v2/authenticator_spec.rb new file mode 100644 index 0000000..39e7779 --- /dev/null +++ b/spec/core/api/v2/authenticator_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +RSpec.describe Bls::Core::V2::Authenticator do + describe "#save!" do + it "returns a authorization scheme" do + basic = Bls::Core::Authorization::Basic.new(token: "test") + auth = Bls::Core::V2::Authenticator.new(authentication: basic) + + result = auth.save! + + expect(result).to respond_to(:header) + end + end + + describe ".build" do + it "sets the authentication" do + api = Bls::Core::V2.new + basic = Bls::Core::Authorization::Basic.new(token: "test") + allow(Bls::Core::Authorization::Basic).to receive(:factory). + and_return(basic) + + result = Bls::Core::V2::Authenticator.build(api) + + expect(result.authentication).to eq(basic) + end + end + + def build_config(environment: "staging", client_id: "cid_123", + client_secret: "cs_123") + OpenStruct.new(environment: environment, client_id: client_id, + client_secret: client_secret) + end +end diff --git a/spec/core/api/v2_spec.rb b/spec/core/api/v2_spec.rb new file mode 100644 index 0000000..80915dc --- /dev/null +++ b/spec/core/api/v2_spec.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +RSpec.describe Bls::Core::V2 do + describe "#base_url" do + context "when the environment is production" do + it "returns the correct url" do + api = Bls::Core::V2.new(environment: "production") + + result = api.base_url + + expect(result).to eq("https://core.batchlinesolutions.com") + end + end + + context "when the environment is not production" do + it "returns the correct url" do + api = Bls::Core::V2.new(environment: "staging") + + result = api.base_url + + expect(result).to eq("https://core.batchlinesolutions.dev") + end + end + end + + describe "#client" do + it "returns a api client" do + api = Bls::Core::V2.new + build_stubbed_authenticator(api) + + result = api.client + + expect(result).to respond_to(:post) + end + end + + describe "#authenticate" do + it "builds a authenticator" do + api = Bls::Core::V2.new + build_stubbed_authenticator(api) + + api.authenticate! + + expect(Bls::Core::V2::Authenticator).to have_received(:build).once + end + + it "authenticates" do + api = Bls::Core::V2.new + authenticator = build_stubbed_authenticator(api) + + api.authenticate! + + expect(authenticator).to have_received(:save!).once + end + + context "when the authentication is present" do + it "is authenticated" do + api = Bls::Core::V2.new + build_stubbed_authenticator(api) + + api.authenticate! + + expect(api).to be_authenticated + end + end + end + + describe "reset authentication" do + it "removes the present authentication" do + auth = Bls::Core::Authorization::Basic.new(token: "1234") + api = Bls::Core::V2.new(authentication: auth) + + result = api.reset_authentication + + expect(result.authentication).to be_blank + end + + it "is not authenticated" do + auth = Bls::Core::Authorization::Basic.new(token: "1234") + api = Bls::Core::V2.new(authentication: auth) + + result = api.reset_authentication + + expect(result).not_to be_authenticated + end + end + + describe ".build" do + it "sets the environment" do + config = build_config(environment: "preview") + + result = Bls::Core::V2.build(config: config) + + expect(result.environment).to eq("preview") + end + + it "sets the username" do + config = build_config(username: "secretUsername") + + result = Bls::Core::V2.build(config: config) + + expect(result.username).to eq("secretUsername") + end + + it "sets the password" do + config = build_config(password: "secretPassword") + + result = Bls::Core::V2.build(config: config) + + expect(result.password).to eq("secretPassword") + end + end + + def build_config(environment: "staging", username: "string", + password: "string") + OpenStruct.new(environment: environment, username: username, + password: password) + end + + def build_stubbed_authenticator(api) + authenticator = Bls::Core::V2::Authenticator.build(api) + authentication = authenticator.authentication + client = Bls::Core::ApiClient.new(authentication: authentication) + allow(Bls::Core::V2::Authenticator).to receive(:build). + and_return(authenticator) + allow(authenticator).to receive(:save!).and_return(client) + authenticator + end +end diff --git a/spec/core/resources/product_spec.rb b/spec/core/resources/product_spec.rb new file mode 100644 index 0000000..2aad42f --- /dev/null +++ b/spec/core/resources/product_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +RSpec.describe Bls::Core::Product do + describe ".retrieve" do + it "returns the product" do + product_id = "MP0527" + body = build_product_response(product_id: product_id) + response = build_response(body: body) + stub_api_v2_authentication + stub_api_response response: response + + result = Bls::Core::Product.retrieve(product_id) + + expect(result.code).to eq(product_id) + end + + it "returns packaged and cooked nutrition facts" do + body = build_product_response + response = build_response(body: body) + stub_api_v2_authentication + stub_api_response response: response + + result = Bls::Core::Product.retrieve("TEST123") + + expect(result.nutrition_facts.keys).to match_array( + [:as_packaged, :as_cooked] + ) + end + end +end diff --git a/spec/support/fixtures/product_response.json b/spec/support/fixtures/product_response.json new file mode 100644 index 0000000..89eba23 --- /dev/null +++ b/spec/support/fixtures/product_response.json @@ -0,0 +1,46 @@ +{ + "code": "string", + "name": "string", + "status": "DRAFT", + "refrigeration_type": "NONE", + "refrigeration_level": "STANDARD", + "unit_volume": { + "value": 0.1 + }, + "unit_slots": { + "value": 0.1 + }, + "ingredients": [ + { + "code": "string", + "name": "string", + "quantity": 0.1, + "unit": "string" + } + ], + "nutrition_facts": { + "as_packaged": [ + { + "code": "CALORIES", + "amount": 0, + "unit": "string" + } + ], + "as_cooked": [ + { + "code": "CALORIES", + "amount": 0, + "unit": "string" + } + ] + }, + "allergens": [ + { + "code": "MILK", + "source": "NONE" + } + ], + "dietary_tags": [ + "PESCATARIAN" + ] +} \ No newline at end of file diff --git a/spec/support/helpers/api_helper.rb b/spec/support/helpers/api_helper.rb index e64195b..39e0f0d 100644 --- a/spec/support/helpers/api_helper.rb +++ b/spec/support/helpers/api_helper.rb @@ -4,6 +4,8 @@ module ApiHelper ERROR_API_RESPONSE = "spec/support/fixtures/error_response.json".freeze INVENTORY_LEVELS_API_RESPONSE = "spec/support/fixtures/inventory_levels_response.json".freeze + PRODUCT_API_RESPONSE = + "spec/support/fixtures/product_response.json".freeze def authentication_response(access_token:) build_response body: { access_token: access_token } @@ -28,6 +30,12 @@ def stub_api_v1_authentication(authenticated: true) allow(api).to receive(:authenticated?).and_return(authenticated) end + def stub_api_v2_authentication(authenticated: true) + api = Bls::Core::V2.build + allow(Bls::Core::V2).to receive(:new).and_return(api) + allow(api).to receive(:authenticated?).and_return(authenticated) + end + def stub_api_response(response: OpenStruct.new(body: "{}", code: 200), http: double) allow(Net::HTTP).to receive(:start).and_yield(http) @@ -64,6 +72,10 @@ def read_inventory_levels_api_response parse_json_file(INVENTORY_LEVELS_API_RESPONSE) end + def read_product_api_response + parse_json_file(PRODUCT_API_RESPONSE) + end + def build_menu_response(menu_id: "2023-01-01", menu_items: []) stubbed_response = read_menu_api_response stubbed_response[:id] = menu_id @@ -79,6 +91,12 @@ def build_inventory_levels_response read_inventory_levels_api_response end + def build_product_response(product_id: "string") + stubbed_response = read_product_api_response + stubbed_response[:code] = product_id + stubbed_response + end + def stubbed_menu_items response = read_menu_api_response response[:items]