mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-03-25 05:45:15 +00:00
Merge pull request #11008 from mkllnk/dfc-connector-import
Prototype: Import products via DFC Connector
This commit is contained in:
@@ -1,12 +1,41 @@
|
||||
# DfcProvider
|
||||
# Data Food Consortium API for the Open Food Network app (OFN DFC API)
|
||||
|
||||
This engine is implementing the Data Food Consortium specifications in order to serve semantic data.
|
||||
You can find more details about this on https://github.com/datafoodconsortium.
|
||||
This engine implements the [Data Food Consortium] specifications. It serves and
|
||||
reads semantic data encoded in JSON-LD.
|
||||
|
||||
Basically, it allows an OFN user linked to an enterprise:
|
||||
* to serve his Products Catalog through a dedicated API using JSON-LD format, structured by the DFC Ontology
|
||||
* to be authenticated thanks to an Access Token from DFC Authorization server (using an OIDC implementation)
|
||||
[Data Food Consortium]: https://github.com/datafoodconsortium
|
||||
|
||||
The API endpoint for the catalog is `/api/dfc_provider/enterprise/prodcuts.json` and you need to pass the token inside an authentication header (`Authentication: Bearer 123mytoken456`).
|
||||
## Authentication
|
||||
|
||||
This feature is still under active development.
|
||||
The DFC uses OpenID Connect (OIDC) to authenticate requests. You need an
|
||||
account with a trusted OIDC provider. Currently these are:
|
||||
|
||||
* https://login.lescommuns.org/auth/
|
||||
|
||||
But you can also authenticate with your OFN user login (session cookie) through
|
||||
your browser.
|
||||
|
||||
## API endpoints
|
||||
|
||||
The API is under development and this list may be out of date.
|
||||
|
||||
```
|
||||
/api/dfc-v1.7/persons/:id
|
||||
* show: firstName, lastName, affiliatedOrganizations
|
||||
|
||||
/api/dfc-v1.7/enterprises/:id
|
||||
* show: name, suppliedProducts, catalogItems
|
||||
|
||||
/api/dfc-v1.7/enterprises/:enterprise_id/supplied_products (index)
|
||||
|
||||
/api/dfc-v1.7/enterprises/:enterprise_id/supplied_products/:id
|
||||
* create: name, description, quantity
|
||||
* show: name, description, productType, quantity
|
||||
* update: description
|
||||
|
||||
/api/dfc-v1.7/enterprises/:enterprise_id/catalog_items (index)
|
||||
|
||||
/api/dfc-v1.7/enterprises/:enterprise_id/catalog_items/:id
|
||||
* show: product, sku, stockLimitation, offers (price, stockLimitation)
|
||||
* update: sku, stockLimitation
|
||||
```
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "data_food_consortium/connector/connector"
|
||||
|
||||
# Controller used to provide the SuppliedProducts API for the DFC application
|
||||
# SuppliedProducts are products that are managed by an enterprise.
|
||||
module DfcProvider
|
||||
class SuppliedProductsController < DfcProvider::BaseController
|
||||
before_action :check_enterprise
|
||||
rescue_from JSON::LD::JsonLdError::LoadingDocumentFailed, with: -> do
|
||||
head :bad_request
|
||||
end
|
||||
|
||||
def create
|
||||
supplied_product = import.first
|
||||
product = SuppliedProductBuilder.import(supplied_product)
|
||||
product.supplier = current_enterprise
|
||||
product.save!
|
||||
end
|
||||
|
||||
def show
|
||||
product = DfcBuilder.supplied_product(variant)
|
||||
product = SuppliedProductBuilder.supplied_product(variant)
|
||||
render json: DfcLoader.connector.export(product)
|
||||
end
|
||||
|
||||
@@ -27,6 +39,10 @@ module DfcProvider
|
||||
|
||||
private
|
||||
|
||||
def import
|
||||
DfcLoader.connector.import(request.body)
|
||||
end
|
||||
|
||||
def variant
|
||||
@variant ||=
|
||||
VariantFetcher.new(current_enterprise).scope.find(params[:id])
|
||||
|
||||
@@ -6,7 +6,7 @@ class DfcBuilder
|
||||
enterprise_id: variant.product.supplier_id,
|
||||
id: variant.id,
|
||||
)
|
||||
product = supplied_product(variant)
|
||||
product = SuppliedProductBuilder.supplied_product(variant)
|
||||
|
||||
DataFoodConsortium::Connector::CatalogItem.new(
|
||||
id, product: product,
|
||||
@@ -16,21 +16,6 @@ class DfcBuilder
|
||||
)
|
||||
end
|
||||
|
||||
def self.supplied_product(variant)
|
||||
id = urls.enterprise_supplied_product_url(
|
||||
enterprise_id: variant.product.supplier_id,
|
||||
id: variant.id,
|
||||
)
|
||||
|
||||
DataFoodConsortium::Connector::SuppliedProduct.new(
|
||||
id,
|
||||
name: variant.name_to_display,
|
||||
description: variant.description,
|
||||
productType: product_type,
|
||||
quantity: QuantitativeValueBuilder.quantity(variant),
|
||||
)
|
||||
end
|
||||
|
||||
def self.offer(variant)
|
||||
# We don't have an endpoint for offers yet and this URL is only a
|
||||
# placeholder for now. The offer is actually affected by order cycle and
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
class EnterpriseBuilder < DfcBuilder
|
||||
def self.enterprise(enterprise)
|
||||
variants = VariantFetcher.new(enterprise).scope.to_a
|
||||
supplied_products = variants.map(&method(:supplied_product))
|
||||
catalog_items = variants.map(&method(:catalog_item))
|
||||
supplied_products = catalog_items.map(&:product)
|
||||
|
||||
DataFoodConsortium::Connector::Enterprise.new(
|
||||
enterprise.name
|
||||
|
||||
@@ -26,4 +26,19 @@ class QuantitativeValueBuilder < DfcBuilder
|
||||
DfcLoader.connector.MEASURES.UNIT.QUANTITYUNIT.PIECE
|
||||
end
|
||||
end
|
||||
|
||||
def self.apply(quantity, product)
|
||||
product.variant_unit, product.variant_unit_name =
|
||||
case quantity.unit
|
||||
when DfcLoader.connector.MEASURES.UNIT.QUANTITYUNIT.LITRE
|
||||
["volume", "liter"]
|
||||
when DfcLoader.connector.MEASURES.UNIT.QUANTITYUNIT.GRAM
|
||||
["weight", "gram"]
|
||||
else
|
||||
["items", "items"]
|
||||
end
|
||||
|
||||
product.variant_unit_scale = 1
|
||||
product.unit_value = quantity.value
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SuppliedProductBuilder < DfcBuilder
|
||||
def self.supplied_product(variant)
|
||||
id = urls.enterprise_supplied_product_url(
|
||||
enterprise_id: variant.product.supplier_id,
|
||||
id: variant.id,
|
||||
)
|
||||
|
||||
DataFoodConsortium::Connector::SuppliedProduct.new(
|
||||
id,
|
||||
name: variant.name_to_display,
|
||||
description: variant.description,
|
||||
productType: product_type,
|
||||
quantity: QuantitativeValueBuilder.quantity(variant),
|
||||
)
|
||||
end
|
||||
|
||||
def self.import(supplied_product)
|
||||
Spree::Product.new(
|
||||
name: supplied_product.name,
|
||||
description: supplied_product.description,
|
||||
price: 0, # will be in DFC Offer
|
||||
primary_taxon: Spree::Taxon.first, # dummy value until we have a mapping
|
||||
shipping_category: DefaultShippingCategory.find_or_create,
|
||||
).tap do |product|
|
||||
QuantitativeValueBuilder.apply(supplied_product.quantity, product)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,7 +3,7 @@
|
||||
DfcProvider::Engine.routes.draw do
|
||||
resources :enterprises, only: [:show] do
|
||||
resources :catalog_items, only: [:index, :show, :update]
|
||||
resources :supplied_products, only: [:show, :update]
|
||||
resources :supplied_products, only: [:create, :show, :update]
|
||||
end
|
||||
resources :persons, only: [:show]
|
||||
end
|
||||
|
||||
@@ -8,6 +8,40 @@ describe "SuppliedProducts", type: :request do
|
||||
let!(:product) { create(:simple_product, supplier: enterprise ) }
|
||||
let!(:variant) { product.variants.first }
|
||||
|
||||
describe :create do
|
||||
let(:endpoint) do
|
||||
enterprise_supplied_products_path(enterprise_id: enterprise.id)
|
||||
end
|
||||
let(:supplied_product) do
|
||||
SuppliedProductBuilder.supplied_product(new_variant)
|
||||
end
|
||||
let(:new_variant) do
|
||||
# We need an id to generate a URL as semantic id when exporting.
|
||||
build(:variant, id: 0, name: "Apple", unit_value: 3)
|
||||
end
|
||||
|
||||
it "flags a bad request" do
|
||||
post endpoint, headers: auth_header(user.uid)
|
||||
|
||||
expect(response).to have_http_status :bad_request
|
||||
end
|
||||
|
||||
it "creates a variant" do
|
||||
request_body = DfcLoader.connector.export(supplied_product)
|
||||
|
||||
expect do
|
||||
post endpoint,
|
||||
params: request_body,
|
||||
headers: auth_header(user.uid)
|
||||
end
|
||||
.to change { enterprise.supplied_products.count }.by(1)
|
||||
|
||||
variant = Spree::Variant.last
|
||||
expect(variant.name).to eq "Apple"
|
||||
expect(variant.unit_value).to eq 3
|
||||
end
|
||||
end
|
||||
|
||||
describe :show do
|
||||
it "returns variants" do
|
||||
get enterprise_supplied_product_path(
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require DfcProvider::Engine.root.join("spec/spec_helper")
|
||||
|
||||
describe EnterpriseBuilder do
|
||||
subject(:builder) { described_class }
|
||||
let(:enterprise) { variant.product.supplier }
|
||||
let(:variant) { create(:product, name: "Apple").variants.first }
|
||||
|
||||
describe ".enterprise" do
|
||||
it "assigns a semantic id" do
|
||||
result = builder.enterprise(enterprise)
|
||||
|
||||
expect(result.semanticId).to eq(
|
||||
"http://test.host/api/dfc-v1.7/enterprises/#{enterprise.id}"
|
||||
)
|
||||
end
|
||||
|
||||
it "assignes products" do
|
||||
result = builder.enterprise(enterprise)
|
||||
|
||||
expect(result.suppliedProducts.count).to eq 1
|
||||
expect(result.suppliedProducts[0].name).to eq "Apple"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
require DfcProvider::Engine.root.join("spec/spec_helper")
|
||||
|
||||
describe DfcBuilder do
|
||||
describe SuppliedProductBuilder do
|
||||
subject(:builder) { described_class }
|
||||
let(:variant) {
|
||||
build(:variant, id: 5).tap { |v| v.product.supplier_id = 7 }
|
||||
}
|
||||
|
||||
describe ".supplied_product" do
|
||||
it "assigns a semantic id" do
|
||||
product = DfcBuilder.supplied_product(variant)
|
||||
product = builder.supplied_product(variant)
|
||||
|
||||
expect(product.semanticId).to eq(
|
||||
"http://test.host/api/dfc-v1.7/enterprises/7/supplied_products/5"
|
||||
@@ -17,7 +18,7 @@ describe DfcBuilder do
|
||||
end
|
||||
|
||||
it "assigns a quantity" do
|
||||
product = DfcBuilder.supplied_product(variant)
|
||||
product = builder.supplied_product(variant)
|
||||
|
||||
expect(product.quantity.value).to eq 1.0
|
||||
expect(product.quantity.unit.semanticId).to eq "dfc-m:Gram"
|
||||
@@ -25,7 +26,7 @@ describe DfcBuilder do
|
||||
|
||||
it "assigns the product name by default" do
|
||||
variant.product.name = "Apple"
|
||||
product = DfcBuilder.supplied_product(variant)
|
||||
product = builder.supplied_product(variant)
|
||||
|
||||
expect(product.name).to eq "Apple"
|
||||
end
|
||||
@@ -33,13 +34,13 @@ describe DfcBuilder do
|
||||
it "assigns the variant name if present" do
|
||||
variant.product.name = "Apple"
|
||||
variant.display_name = "Granny Smith"
|
||||
product = DfcBuilder.supplied_product(variant)
|
||||
product = builder.supplied_product(variant)
|
||||
|
||||
expect(product.name).to eq "Granny Smith"
|
||||
end
|
||||
|
||||
it "assigns a product type" do
|
||||
product = DfcBuilder.supplied_product(variant)
|
||||
product = builder.supplied_product(variant)
|
||||
vegetable = DfcLoader.connector.PRODUCT_TYPES.VEGETABLE.NON_LOCAL_VEGETABLE
|
||||
|
||||
expect(product.productType).to eq vegetable
|
||||
|
||||
@@ -8,11 +8,4 @@ RSpec.configure do |config|
|
||||
config.include AuthorizationHelper, type: :request
|
||||
config.include DfcProvider::Engine.routes.url_helpers, type: :request
|
||||
config.include Warden::Test::Helpers, type: :request
|
||||
|
||||
config.around(:each) do |example|
|
||||
# The DFC Connector fetches the context when loaded.
|
||||
VCR.use_cassette("dfc-context") do
|
||||
example.run
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
13
lib/data_food_consortium/connector/connector.rb
Normal file
13
lib/data_food_consortium/connector/connector.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "importer"
|
||||
|
||||
module DataFoodConsortium
|
||||
module Connector
|
||||
class Connector
|
||||
def import(json_string_or_io)
|
||||
Importer.new.import(json_string_or_io)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
116
lib/data_food_consortium/connector/importer.rb
Normal file
116
lib/data_food_consortium/connector/importer.rb
Normal file
@@ -0,0 +1,116 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "skos_parser"
|
||||
|
||||
module DataFoodConsortium
|
||||
module Connector
|
||||
class Importer
|
||||
TYPES = [
|
||||
DataFoodConsortium::Connector::CatalogItem,
|
||||
DataFoodConsortium::Connector::Enterprise,
|
||||
DataFoodConsortium::Connector::Offer,
|
||||
DataFoodConsortium::Connector::Person,
|
||||
DataFoodConsortium::Connector::QuantitativeValue,
|
||||
DataFoodConsortium::Connector::SuppliedProduct,
|
||||
].freeze
|
||||
|
||||
def self.type_map
|
||||
@type_map ||= TYPES.each_with_object({}) do |clazz, result|
|
||||
# Methods with variable arguments have a negative arity of -n-1
|
||||
# where n is the number of required arguments.
|
||||
number_of_required_args = -1 * (clazz.instance_method(:initialize).arity + 1)
|
||||
args = Array.new(number_of_required_args)
|
||||
type_uri = clazz.new(*args).semanticType
|
||||
result[type_uri] = clazz
|
||||
end
|
||||
end
|
||||
|
||||
def import(json_string_or_io)
|
||||
@subjects = {}
|
||||
|
||||
graph = parse_rdf(json_string_or_io)
|
||||
build_subjects(graph)
|
||||
apply_statements(graph)
|
||||
|
||||
if @subjects.size > 1
|
||||
@subjects.values
|
||||
else
|
||||
@subjects.values.first
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# The `io` parameter can be a String or an IO instance.
|
||||
def parse_rdf(io)
|
||||
io = StringIO.new(io) if io.is_a?(String)
|
||||
RDF::Graph.new << JSON::LD::API.toRdf(io)
|
||||
end
|
||||
|
||||
def build_subjects(graph)
|
||||
graph.query({ predicate: RDF.type }).each do |statement|
|
||||
@subjects[statement.subject] = build_subject(statement)
|
||||
end
|
||||
end
|
||||
|
||||
def build_subject(type_statement)
|
||||
# Not all subjects have an id, some are anonymous.
|
||||
id = type_statement.subject.try(:value)
|
||||
type = type_statement.object.value
|
||||
clazz = self.class.type_map[type]
|
||||
|
||||
clazz.new(*[id].compact)
|
||||
end
|
||||
|
||||
def apply_statements(statements)
|
||||
statements.each do |statement|
|
||||
apply_statement(statement)
|
||||
end
|
||||
end
|
||||
|
||||
def apply_statement(statement)
|
||||
subject = subject_of(statement)
|
||||
property_id = statement.predicate.value
|
||||
value = resolve_object(statement.object)
|
||||
|
||||
return unless subject.hasSemanticProperty?(property_id)
|
||||
|
||||
property = subject.__send__(:findSemanticProperty, property_id)
|
||||
|
||||
if property.value.is_a?(Enumerable)
|
||||
property.value << value
|
||||
else
|
||||
setter = guess_setter_name(statement.predicate)
|
||||
subject.try(setter, value) if setter
|
||||
end
|
||||
end
|
||||
|
||||
def subject_of(statement)
|
||||
@subjects[statement.subject]
|
||||
end
|
||||
|
||||
def resolve_object(object)
|
||||
@subjects[object] || skos_concept(object) || object.object
|
||||
end
|
||||
|
||||
def skos_concept(object)
|
||||
return unless object.uri?
|
||||
|
||||
id = object.value.sub(
|
||||
"http://static.datafoodconsortium.org/data/measures.rdf#", "dfc-m:"
|
||||
)
|
||||
SKOSParser.concepts[id]
|
||||
end
|
||||
|
||||
def guess_setter_name(predicate)
|
||||
fragment = predicate.fragment
|
||||
|
||||
# Some predicates are named like `hasQuantity`
|
||||
# but the attribute name would be `quantity`.
|
||||
name = fragment.sub(/^has/, "").camelize(:lower)
|
||||
|
||||
"#{name}="
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
24
lib/data_food_consortium/connector/skos_parser.rb
Normal file
24
lib/data_food_consortium/connector/skos_parser.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Overriding the current implementation to store all parsed concepts for
|
||||
# lookup later. Otherwise the importer can't associate these.
|
||||
# This is just a workaround and needs to be solved upstream.
|
||||
module DataFoodConsortium
|
||||
module Connector
|
||||
class SKOSParser
|
||||
def self.concepts
|
||||
@concepts ||= {}
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def createSKOSConcept(element) # rubocop:disable Naming/MethodName
|
||||
concept = DataFoodConsortium::Connector::SKOSConcept.new
|
||||
concept.semanticId = element.id
|
||||
concept.semanticType = element.type
|
||||
self.class.concepts[element.id] = concept
|
||||
concept
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -91,6 +91,13 @@ RSpec.configure do |config|
|
||||
expectations.syntax = :expect
|
||||
end
|
||||
|
||||
config.around(:each) do |example|
|
||||
# The DFC Connector fetches the context when loaded.
|
||||
VCR.use_cassette("dfc-context") do
|
||||
example.run
|
||||
end
|
||||
end
|
||||
|
||||
# Enable caching in any specs tagged with `caching: true`.
|
||||
config.around(:each, :caching) do |example|
|
||||
caching = ActionController::Base.perform_caching
|
||||
|
||||
40
spec/lib/data_food_consortium/connector/connector_spec.rb
Normal file
40
spec/lib/data_food_consortium/connector/connector_spec.rb
Normal file
@@ -0,0 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require Rails.root.join('lib/data_food_consortium/connector/connector')
|
||||
|
||||
describe DataFoodConsortium::Connector::Connector, vcr: true do
|
||||
subject(:connector) { described_class.instance }
|
||||
let(:product) do
|
||||
DataFoodConsortium::Connector::SuppliedProduct.new(
|
||||
"https://example.net/tomato",
|
||||
name: "Tomato",
|
||||
description: "Awesome tomato"
|
||||
)
|
||||
end
|
||||
|
||||
it "exports" do
|
||||
json = connector.export(product)
|
||||
expect(json).to match '"dfc-b:name":"Tomato"'
|
||||
end
|
||||
|
||||
it "imports" do
|
||||
json = connector.export(product)
|
||||
result = connector.import(json)
|
||||
expect(result.class).to eq product.class
|
||||
expect(result.semanticType).to eq product.semanticType
|
||||
expect(result.semanticId).to eq "https://example.net/tomato"
|
||||
expect(result.name).to eq "Tomato"
|
||||
end
|
||||
|
||||
it "imports from IO like Rails supplies it" do
|
||||
json = connector.export(product)
|
||||
io = StringIO.new(json)
|
||||
result = connector.import(io)
|
||||
|
||||
expect(result.class).to eq product.class
|
||||
expect(result.semanticType).to eq product.semanticType
|
||||
expect(result.semanticId).to eq "https://example.net/tomato"
|
||||
expect(result.name).to eq "Tomato"
|
||||
end
|
||||
end
|
||||
105
spec/lib/data_food_consortium/connector/importer_spec.rb
Normal file
105
spec/lib/data_food_consortium/connector/importer_spec.rb
Normal file
@@ -0,0 +1,105 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require Rails.root.join('lib/data_food_consortium/connector/connector')
|
||||
|
||||
describe DataFoodConsortium::Connector::Importer, vcr: true do
|
||||
let(:connector) { DataFoodConsortium::Connector::Connector.instance }
|
||||
let(:enterprise) do
|
||||
DataFoodConsortium::Connector::Enterprise.new(
|
||||
"https://example.net/foo-food-inc",
|
||||
suppliedProducts: [product, second_product],
|
||||
)
|
||||
end
|
||||
let(:catalog_item) do
|
||||
DataFoodConsortium::Connector::CatalogItem.new(
|
||||
"https://example.net/tomatoItem",
|
||||
product:,
|
||||
)
|
||||
end
|
||||
let(:product) do
|
||||
DataFoodConsortium::Connector::SuppliedProduct.new(
|
||||
"https://example.net/tomato",
|
||||
name: "Tomato",
|
||||
description: "Awesome tomato",
|
||||
totalTheoreticalStock: 3,
|
||||
)
|
||||
end
|
||||
let(:second_product) do
|
||||
DataFoodConsortium::Connector::SuppliedProduct.new(
|
||||
"https://example.net/ocra",
|
||||
name: "Ocra",
|
||||
)
|
||||
end
|
||||
let(:quantity) do
|
||||
DataFoodConsortium::Connector::QuantitativeValue.new(
|
||||
unit: piece,
|
||||
value: 5,
|
||||
)
|
||||
end
|
||||
let(:piece) do
|
||||
unless connector.MEASURES.respond_to?(:UNIT)
|
||||
connector.loadMeasures(read_file("measures"))
|
||||
end
|
||||
connector.MEASURES.UNIT.QUANTITYUNIT.PIECE
|
||||
end
|
||||
|
||||
it "imports a single object with simple properties" do
|
||||
result = import(product)
|
||||
|
||||
expect(result.class).to eq product.class
|
||||
expect(result.semanticType).to eq product.semanticType
|
||||
expect(result.semanticId).to eq "https://example.net/tomato"
|
||||
expect(result.name).to eq "Tomato"
|
||||
expect(result.description).to eq "Awesome tomato"
|
||||
expect(result.totalTheoreticalStock).to eq 3
|
||||
end
|
||||
|
||||
it "imports a graph with multiple objects" do
|
||||
result = import(catalog_item, product)
|
||||
|
||||
expect(result).to be_a Array
|
||||
expect(result.size).to eq 2
|
||||
|
||||
item, tomato = result
|
||||
|
||||
expect(item.class).to eq catalog_item.class
|
||||
expect(item.semanticType).to eq catalog_item.semanticType
|
||||
expect(item.semanticId).to eq "https://example.net/tomatoItem"
|
||||
expect(tomato.name).to eq "Tomato"
|
||||
expect(tomato.description).to eq "Awesome tomato"
|
||||
expect(tomato.totalTheoreticalStock).to eq 3
|
||||
end
|
||||
|
||||
it "imports a graph including anonymous objects" do
|
||||
product.quantity = quantity
|
||||
|
||||
tomato, items = import(product)
|
||||
|
||||
expect(tomato.name).to eq "Tomato"
|
||||
expect(tomato.quantity).to eq items
|
||||
expect(items.value).to eq 5
|
||||
expect(items.unit).to eq piece
|
||||
end
|
||||
|
||||
it "imports properties with lists" do
|
||||
result = import(enterprise, product, second_product)
|
||||
|
||||
expect(result.size).to eq 3
|
||||
|
||||
enterprise, tomato, ocra = result
|
||||
|
||||
expect(enterprise.suppliedProducts).to eq [tomato, ocra]
|
||||
end
|
||||
|
||||
def import(*args)
|
||||
json = connector.export(*args)
|
||||
connector.import(json)
|
||||
end
|
||||
|
||||
def read_file(name)
|
||||
JSON.parse(
|
||||
Rails.root.join("engines/dfc_provider/vendor/#{name}.json").read
|
||||
)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user