From 3b5b9ec54d13c00d2f093ab545acddbbbd0b204e Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 21 Jul 2023 16:19:35 +1000 Subject: [PATCH] Avoid network request to DFC context on export This protects us from the DFC website going down or the DFC updating the context with breaking changes. We are in control of updating the context now (opt-in to newer versions). --- .../dfc_provider/catalog_items_controller.rb | 4 +- .../dfc_provider/enterprises_controller.rb | 2 +- .../dfc_provider/persons_controller.rb | 2 +- .../supplied_products_controller.rb | 4 +- engines/dfc_provider/app/services/dfc_io.rb | 60 +++ .../dfc_provider/spec/services/dfc_io_spec.rb | 37 ++ .../spec/services/dfc_loader_spec.rb | 4 +- spec/base_spec_helper.rb | 2 +- swagger/dfc-v1.7/swagger.yaml | 348 +++++++++++++++++- 9 files changed, 447 insertions(+), 16 deletions(-) create mode 100644 engines/dfc_provider/app/services/dfc_io.rb create mode 100644 engines/dfc_provider/spec/services/dfc_io_spec.rb diff --git a/engines/dfc_provider/app/controllers/dfc_provider/catalog_items_controller.rb b/engines/dfc_provider/app/controllers/dfc_provider/catalog_items_controller.rb index 03a85910b0..9dfa9e51a4 100644 --- a/engines/dfc_provider/app/controllers/dfc_provider/catalog_items_controller.rb +++ b/engines/dfc_provider/app/controllers/dfc_provider/catalog_items_controller.rb @@ -8,7 +8,7 @@ module DfcProvider def index person = PersonBuilder.person(current_user) - render json: DfcLoader.connector.export( + render json: DfcIo.export( person, *person.affiliatedOrganizations, *person.affiliatedOrganizations.flat_map(&:catalogItems), @@ -20,7 +20,7 @@ module DfcProvider def show catalog_item = DfcBuilder.catalog_item(variant) offers = catalog_item.offers - render json: DfcLoader.connector.export(catalog_item, *offers) + render json: DfcIo.export(catalog_item, *offers) end def update diff --git a/engines/dfc_provider/app/controllers/dfc_provider/enterprises_controller.rb b/engines/dfc_provider/app/controllers/dfc_provider/enterprises_controller.rb index ffbd82dfb5..df04f708a3 100644 --- a/engines/dfc_provider/app/controllers/dfc_provider/enterprises_controller.rb +++ b/engines/dfc_provider/app/controllers/dfc_provider/enterprises_controller.rb @@ -7,7 +7,7 @@ module DfcProvider def show enterprise = EnterpriseBuilder.enterprise(current_enterprise) - render json: DfcLoader.connector.export( + render json: DfcIo.export( enterprise, *enterprise.suppliedProducts, *enterprise.catalogItems, diff --git a/engines/dfc_provider/app/controllers/dfc_provider/persons_controller.rb b/engines/dfc_provider/app/controllers/dfc_provider/persons_controller.rb index d88eea729f..20f7623a5a 100644 --- a/engines/dfc_provider/app/controllers/dfc_provider/persons_controller.rb +++ b/engines/dfc_provider/app/controllers/dfc_provider/persons_controller.rb @@ -7,7 +7,7 @@ module DfcProvider def show person = PersonBuilder.person(user) - render json: DfcLoader.connector.export(person) + render json: DfcIo.export(person) end private diff --git a/engines/dfc_provider/app/controllers/dfc_provider/supplied_products_controller.rb b/engines/dfc_provider/app/controllers/dfc_provider/supplied_products_controller.rb index 64473a925b..5cee7c9f23 100644 --- a/engines/dfc_provider/app/controllers/dfc_provider/supplied_products_controller.rb +++ b/engines/dfc_provider/app/controllers/dfc_provider/supplied_products_controller.rb @@ -23,12 +23,12 @@ module DfcProvider supplied_product = SuppliedProductBuilder.supplied_product( product.variants.first ) - render json: DfcLoader.connector.export(supplied_product) + render json: DfcIo.export(supplied_product) end def show product = SuppliedProductBuilder.supplied_product(variant) - render json: DfcLoader.connector.export(product) + render json: DfcIo.export(product) end def update diff --git a/engines/dfc_provider/app/services/dfc_io.rb b/engines/dfc_provider/app/services/dfc_io.rb new file mode 100644 index 0000000000..8749c9735f --- /dev/null +++ b/engines/dfc_provider/app/services/dfc_io.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +# Our interface to the DFC Connector library. +module DfcIo + CONTEXT = JSON.parse <<~JSON + { + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "skos" : "http://www.w3.org/2004/02/skos/core#", + "dfc": "http://static.datafoodconsortium.org/ontologies/DFC_FullModel.owl#", + "dc": "http://purl.org/dc/elements/1.1/#", + "dfc-b": "http://static.datafoodconsortium.org/ontologies/DFC_BusinessOntology.owl#", + "dfc-p": "http://static.datafoodconsortium.org/ontologies/DFC_ProductOntology.owl#", + "dfc-t": "http://static.datafoodconsortium.org/ontologies/DFC_TechnicalOntology.owl#", + "dfc-m": "http://static.datafoodconsortium.org/data/measures.rdf#", + "dfc-pt": "http://static.datafoodconsortium.org/data/productTypes.rdf#", + "dfc-f": "http://static.datafoodconsortium.org/data/facets.rdf#", + "dfc-p:hasUnit":{ "@type":"@id" }, + "dfc-b:hasUnit":{ "@type":"@id" }, + "dfc-b:hasQuantity":{ "@type":"@id" }, + "dfc-p:hasType":{ "@type":"@id" }, + "dfc-b:hasType":{ "@type":"@id" }, + "dfc-b:references":{ "@type":"@id" }, + "dfc-b:referencedBy":{ "@type":"@id" }, + "dfc-b:offeres":{ "@type":"@id" }, + "dfc-b:supplies":{ "@type":"@id" }, + "dfc-b:defines":{ "@type":"@id" }, + "dfc-b:affiliates":{ "@type":"@id" }, + "dfc-b:hasQuantity":{ "@type":"@id" }, + "dfc-b:manages":{ "@type":"@id" }, + "dfc-b:offeredThrough":{ "@type":"@id" }, + "dfc-b:hasBrand":{ "@type":"@id" }, + "dfc-b:hasGeographicalOrigin":{ "@type":"@id" }, + "dfc-b:hasClaim":{ "@type":"@id" }, + "dfc-b:hasAllergenDimension":{ "@type":"@id" }, + "dfc-b:hasNutrimentDimension":{ "@type":"@id" }, + "dfc-b:hasPhysicalDimension":{ "@type":"@id" }, + "dfc:owner":{ "@type":"@id" }, + "dfc-t:hostedBy":{ "@type":"@id" }, + "dfc-t:hasPivot":{ "@type":"@id" }, + "dfc-t:represent":{ "@type":"@id" } + } + JSON + + # Serialise DFC Connector subjects as JSON-LD string. + # + # This is a re-implementation of the Connector.export to provide our own context. + def self.export(*subjects) + return "" if subjects.empty? + + serializer = VirtualAssembly::Semantizer::HashSerializer.new(CONTEXT) + + hashes = subjects.map do |subject| + # JSON::LD needs a context on every input using prefixes. + subject.serialize(serializer).merge("@context" => CONTEXT) + end + + json_ld = JSON::LD::API.compact(hashes, CONTEXT) + JSON.generate(json_ld) + end +end diff --git a/engines/dfc_provider/spec/services/dfc_io_spec.rb b/engines/dfc_provider/spec/services/dfc_io_spec.rb new file mode 100644 index 0000000000..9e8944f49a --- /dev/null +++ b/engines/dfc_provider/spec/services/dfc_io_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require DfcProvider::Engine.root.join("spec/spec_helper") + +describe DfcIo do + let(:person) do + DataFoodConsortium::Connector::Person.new("Pete") + end + let(:enterprise) do + DataFoodConsortium::Connector::Enterprise.new("Pete's Pumpkins") + end + + describe ".export" do + it "exports nothing" do + expect(DfcIo.export).to eq "" + end + + it "embeds the context" do + json = DfcIo.export(person) + result = JSON.parse(json) + + expect(result["@context"]).to be_a Hash + end + + it "uses the context to shorten URIs" do + person.affiliatedOrganizations << enterprise + + json = DfcIo.export(person, enterprise) + result = JSON.parse(json) + + expect(result["@graph"].count).to eq 2 + expect(result["@graph"].first.keys).to include( + *%w(@id @type dfc-b:affiliates) + ) + end + end +end diff --git a/engines/dfc_provider/spec/services/dfc_loader_spec.rb b/engines/dfc_provider/spec/services/dfc_loader_spec.rb index 1e0558b872..a011c79e47 100644 --- a/engines/dfc_provider/spec/services/dfc_loader_spec.rb +++ b/engines/dfc_provider/spec/services/dfc_loader_spec.rb @@ -4,8 +4,6 @@ require DfcProvider::Engine.root.join("spec/spec_helper") describe DfcLoader do it "prepares the DFC Connector to provide DFC object classes for export" do - connector = DfcLoader.connector - tomato = DataFoodConsortium::Connector::SuppliedProduct.new( "https://openfoodnetwork.org/tomato", name: "Tomato", @@ -15,7 +13,7 @@ describe DfcLoader do expect(tomato.name).to eq "Tomato" expect(tomato.description).to eq "Awesome tomato" - json = connector.export(tomato) + json = DfcIo.export(tomato) result = JSON.parse(json) expect(result.keys).to include( diff --git a/spec/base_spec_helper.rb b/spec/base_spec_helper.rb index 23b47476ac..640496648f 100644 --- a/spec/base_spec_helper.rb +++ b/spec/base_spec_helper.rb @@ -91,7 +91,7 @@ RSpec.configure do |config| expectations.syntax = :expect end - config.around(:each) do |example| + config.around(:each, vcr: true) do |example| # The DFC Connector fetches the context when loaded. VCR.use_cassette("dfc-context") do example.run diff --git a/swagger/dfc-v1.7/swagger.yaml b/swagger/dfc-v1.7/swagger.yaml index c0ef465254..b2e224683c 100644 --- a/swagger/dfc-v1.7/swagger.yaml +++ b/swagger/dfc-v1.7/swagger.yaml @@ -57,7 +57,63 @@ paths: examples: test_example: value: - "@context": http://static.datafoodconsortium.org/ontologies/context.json + "@context": + rdfs: http://www.w3.org/2000/01/rdf-schema# + skos: http://www.w3.org/2004/02/skos/core# + dfc: http://static.datafoodconsortium.org/ontologies/DFC_FullModel.owl# + dc: http://purl.org/dc/elements/1.1/# + dfc-b: http://static.datafoodconsortium.org/ontologies/DFC_BusinessOntology.owl# + dfc-p: http://static.datafoodconsortium.org/ontologies/DFC_ProductOntology.owl# + dfc-t: http://static.datafoodconsortium.org/ontologies/DFC_TechnicalOntology.owl# + dfc-m: http://static.datafoodconsortium.org/data/measures.rdf# + dfc-pt: http://static.datafoodconsortium.org/data/productTypes.rdf# + dfc-f: http://static.datafoodconsortium.org/data/facets.rdf# + dfc-p:hasUnit: + "@type": "@id" + dfc-b:hasUnit: + "@type": "@id" + dfc-b:hasQuantity: + "@type": "@id" + dfc-p:hasType: + "@type": "@id" + dfc-b:hasType: + "@type": "@id" + dfc-b:references: + "@type": "@id" + dfc-b:referencedBy: + "@type": "@id" + dfc-b:offeres: + "@type": "@id" + dfc-b:supplies: + "@type": "@id" + dfc-b:defines: + "@type": "@id" + dfc-b:affiliates: + "@type": "@id" + dfc-b:manages: + "@type": "@id" + dfc-b:offeredThrough: + "@type": "@id" + dfc-b:hasBrand: + "@type": "@id" + dfc-b:hasGeographicalOrigin: + "@type": "@id" + dfc-b:hasClaim: + "@type": "@id" + dfc-b:hasAllergenDimension: + "@type": "@id" + dfc-b:hasNutrimentDimension: + "@type": "@id" + dfc-b:hasPhysicalDimension: + "@type": "@id" + dfc:owner: + "@type": "@id" + dfc-t:hostedBy: + "@type": "@id" + dfc-t:hasPivot: + "@type": "@id" + dfc-t:represent: + "@type": "@id" "@graph": - "@id": http://test.host/api/dfc-v1.7/persons/12345 "@type": dfc-b:Person @@ -116,7 +172,63 @@ paths: examples: test_example: value: - "@context": http://static.datafoodconsortium.org/ontologies/context.json + "@context": + rdfs: http://www.w3.org/2000/01/rdf-schema# + skos: http://www.w3.org/2004/02/skos/core# + dfc: http://static.datafoodconsortium.org/ontologies/DFC_FullModel.owl# + dc: http://purl.org/dc/elements/1.1/# + dfc-b: http://static.datafoodconsortium.org/ontologies/DFC_BusinessOntology.owl# + dfc-p: http://static.datafoodconsortium.org/ontologies/DFC_ProductOntology.owl# + dfc-t: http://static.datafoodconsortium.org/ontologies/DFC_TechnicalOntology.owl# + dfc-m: http://static.datafoodconsortium.org/data/measures.rdf# + dfc-pt: http://static.datafoodconsortium.org/data/productTypes.rdf# + dfc-f: http://static.datafoodconsortium.org/data/facets.rdf# + dfc-p:hasUnit: + "@type": "@id" + dfc-b:hasUnit: + "@type": "@id" + dfc-b:hasQuantity: + "@type": "@id" + dfc-p:hasType: + "@type": "@id" + dfc-b:hasType: + "@type": "@id" + dfc-b:references: + "@type": "@id" + dfc-b:referencedBy: + "@type": "@id" + dfc-b:offeres: + "@type": "@id" + dfc-b:supplies: + "@type": "@id" + dfc-b:defines: + "@type": "@id" + dfc-b:affiliates: + "@type": "@id" + dfc-b:manages: + "@type": "@id" + dfc-b:offeredThrough: + "@type": "@id" + dfc-b:hasBrand: + "@type": "@id" + dfc-b:hasGeographicalOrigin: + "@type": "@id" + dfc-b:hasClaim: + "@type": "@id" + dfc-b:hasAllergenDimension: + "@type": "@id" + dfc-b:hasNutrimentDimension: + "@type": "@id" + dfc-b:hasPhysicalDimension: + "@type": "@id" + dfc:owner: + "@type": "@id" + dfc-t:hostedBy: + "@type": "@id" + dfc-t:hasPivot: + "@type": "@id" + dfc-t:represent: + "@type": "@id" "@graph": - "@id": http://test.host/api/dfc-v1.7/enterprises/10000/catalog_items/10001 "@type": dfc-b:CatalogItem @@ -217,7 +329,63 @@ paths: examples: test_example: value: - "@context": http://static.datafoodconsortium.org/ontologies/context.json + "@context": + rdfs: http://www.w3.org/2000/01/rdf-schema# + skos: http://www.w3.org/2004/02/skos/core# + dfc: http://static.datafoodconsortium.org/ontologies/DFC_FullModel.owl# + dc: http://purl.org/dc/elements/1.1/# + dfc-b: http://static.datafoodconsortium.org/ontologies/DFC_BusinessOntology.owl# + dfc-p: http://static.datafoodconsortium.org/ontologies/DFC_ProductOntology.owl# + dfc-t: http://static.datafoodconsortium.org/ontologies/DFC_TechnicalOntology.owl# + dfc-m: http://static.datafoodconsortium.org/data/measures.rdf# + dfc-pt: http://static.datafoodconsortium.org/data/productTypes.rdf# + dfc-f: http://static.datafoodconsortium.org/data/facets.rdf# + dfc-p:hasUnit: + "@type": "@id" + dfc-b:hasUnit: + "@type": "@id" + dfc-b:hasQuantity: + "@type": "@id" + dfc-p:hasType: + "@type": "@id" + dfc-b:hasType: + "@type": "@id" + dfc-b:references: + "@type": "@id" + dfc-b:referencedBy: + "@type": "@id" + dfc-b:offeres: + "@type": "@id" + dfc-b:supplies: + "@type": "@id" + dfc-b:defines: + "@type": "@id" + dfc-b:affiliates: + "@type": "@id" + dfc-b:manages: + "@type": "@id" + dfc-b:offeredThrough: + "@type": "@id" + dfc-b:hasBrand: + "@type": "@id" + dfc-b:hasGeographicalOrigin: + "@type": "@id" + dfc-b:hasClaim: + "@type": "@id" + dfc-b:hasAllergenDimension: + "@type": "@id" + dfc-b:hasNutrimentDimension: + "@type": "@id" + dfc-b:hasPhysicalDimension: + "@type": "@id" + dfc:owner: + "@type": "@id" + dfc-t:hostedBy: + "@type": "@id" + dfc-t:hasPivot: + "@type": "@id" + dfc-t:represent: + "@type": "@id" "@graph": - "@id": http://test.host/api/dfc-v1.7/enterprises/10000 "@type": dfc-b:Enterprise @@ -264,7 +432,63 @@ paths: examples: test_example: value: - "@context": http://static.datafoodconsortium.org/ontologies/context.json + "@context": + rdfs: http://www.w3.org/2000/01/rdf-schema# + skos: http://www.w3.org/2004/02/skos/core# + dfc: http://static.datafoodconsortium.org/ontologies/DFC_FullModel.owl# + dc: http://purl.org/dc/elements/1.1/# + dfc-b: http://static.datafoodconsortium.org/ontologies/DFC_BusinessOntology.owl# + dfc-p: http://static.datafoodconsortium.org/ontologies/DFC_ProductOntology.owl# + dfc-t: http://static.datafoodconsortium.org/ontologies/DFC_TechnicalOntology.owl# + dfc-m: http://static.datafoodconsortium.org/data/measures.rdf# + dfc-pt: http://static.datafoodconsortium.org/data/productTypes.rdf# + dfc-f: http://static.datafoodconsortium.org/data/facets.rdf# + dfc-p:hasUnit: + "@type": "@id" + dfc-b:hasUnit: + "@type": "@id" + dfc-b:hasQuantity: + "@type": "@id" + dfc-p:hasType: + "@type": "@id" + dfc-b:hasType: + "@type": "@id" + dfc-b:references: + "@type": "@id" + dfc-b:referencedBy: + "@type": "@id" + dfc-b:offeres: + "@type": "@id" + dfc-b:supplies: + "@type": "@id" + dfc-b:defines: + "@type": "@id" + dfc-b:affiliates: + "@type": "@id" + dfc-b:manages: + "@type": "@id" + dfc-b:offeredThrough: + "@type": "@id" + dfc-b:hasBrand: + "@type": "@id" + dfc-b:hasGeographicalOrigin: + "@type": "@id" + dfc-b:hasClaim: + "@type": "@id" + dfc-b:hasAllergenDimension: + "@type": "@id" + dfc-b:hasNutrimentDimension: + "@type": "@id" + dfc-b:hasPhysicalDimension: + "@type": "@id" + dfc:owner: + "@type": "@id" + dfc-t:hostedBy: + "@type": "@id" + dfc-t:hasPivot: + "@type": "@id" + dfc-t:represent: + "@type": "@id" "@id": http://test.host/api/dfc-v1.7/persons/10000 "@type": dfc-b:Person '404': @@ -289,7 +513,63 @@ paths: examples: test_example: value: - "@context": http://static.datafoodconsortium.org/ontologies/context.json + "@context": + rdfs: http://www.w3.org/2000/01/rdf-schema# + skos: http://www.w3.org/2004/02/skos/core# + dfc: http://static.datafoodconsortium.org/ontologies/DFC_FullModel.owl# + dc: http://purl.org/dc/elements/1.1/# + dfc-b: http://static.datafoodconsortium.org/ontologies/DFC_BusinessOntology.owl# + dfc-p: http://static.datafoodconsortium.org/ontologies/DFC_ProductOntology.owl# + dfc-t: http://static.datafoodconsortium.org/ontologies/DFC_TechnicalOntology.owl# + dfc-m: http://static.datafoodconsortium.org/data/measures.rdf# + dfc-pt: http://static.datafoodconsortium.org/data/productTypes.rdf# + dfc-f: http://static.datafoodconsortium.org/data/facets.rdf# + dfc-p:hasUnit: + "@type": "@id" + dfc-b:hasUnit: + "@type": "@id" + dfc-b:hasQuantity: + "@type": "@id" + dfc-p:hasType: + "@type": "@id" + dfc-b:hasType: + "@type": "@id" + dfc-b:references: + "@type": "@id" + dfc-b:referencedBy: + "@type": "@id" + dfc-b:offeres: + "@type": "@id" + dfc-b:supplies: + "@type": "@id" + dfc-b:defines: + "@type": "@id" + dfc-b:affiliates: + "@type": "@id" + dfc-b:manages: + "@type": "@id" + dfc-b:offeredThrough: + "@type": "@id" + dfc-b:hasBrand: + "@type": "@id" + dfc-b:hasGeographicalOrigin: + "@type": "@id" + dfc-b:hasClaim: + "@type": "@id" + dfc-b:hasAllergenDimension: + "@type": "@id" + dfc-b:hasNutrimentDimension: + "@type": "@id" + dfc-b:hasPhysicalDimension: + "@type": "@id" + dfc:owner: + "@type": "@id" + dfc-t:hostedBy: + "@type": "@id" + dfc-t:hasPivot: + "@type": "@id" + dfc-t:represent: + "@type": "@id" "@id": http://test.host/api/dfc-v1.7/enterprises/10000/supplied_products/10001 "@type": dfc-b:SuppliedProduct dfc-b:name: Apple @@ -347,7 +627,63 @@ paths: examples: test_example: value: - "@context": http://static.datafoodconsortium.org/ontologies/context.json + "@context": + rdfs: http://www.w3.org/2000/01/rdf-schema# + skos: http://www.w3.org/2004/02/skos/core# + dfc: http://static.datafoodconsortium.org/ontologies/DFC_FullModel.owl# + dc: http://purl.org/dc/elements/1.1/# + dfc-b: http://static.datafoodconsortium.org/ontologies/DFC_BusinessOntology.owl# + dfc-p: http://static.datafoodconsortium.org/ontologies/DFC_ProductOntology.owl# + dfc-t: http://static.datafoodconsortium.org/ontologies/DFC_TechnicalOntology.owl# + dfc-m: http://static.datafoodconsortium.org/data/measures.rdf# + dfc-pt: http://static.datafoodconsortium.org/data/productTypes.rdf# + dfc-f: http://static.datafoodconsortium.org/data/facets.rdf# + dfc-p:hasUnit: + "@type": "@id" + dfc-b:hasUnit: + "@type": "@id" + dfc-b:hasQuantity: + "@type": "@id" + dfc-p:hasType: + "@type": "@id" + dfc-b:hasType: + "@type": "@id" + dfc-b:references: + "@type": "@id" + dfc-b:referencedBy: + "@type": "@id" + dfc-b:offeres: + "@type": "@id" + dfc-b:supplies: + "@type": "@id" + dfc-b:defines: + "@type": "@id" + dfc-b:affiliates: + "@type": "@id" + dfc-b:manages: + "@type": "@id" + dfc-b:offeredThrough: + "@type": "@id" + dfc-b:hasBrand: + "@type": "@id" + dfc-b:hasGeographicalOrigin: + "@type": "@id" + dfc-b:hasClaim: + "@type": "@id" + dfc-b:hasAllergenDimension: + "@type": "@id" + dfc-b:hasNutrimentDimension: + "@type": "@id" + dfc-b:hasPhysicalDimension: + "@type": "@id" + dfc:owner: + "@type": "@id" + dfc-t:hostedBy: + "@type": "@id" + dfc-t:hasPivot: + "@type": "@id" + dfc-t:represent: + "@type": "@id" "@id": http://test.host/api/dfc-v1.7/enterprises/10000/supplied_products/10001 "@type": dfc-b:SuppliedProduct dfc-b:name: Pesto