mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-24 20:36:49 +00:00
Merge pull request #5231 from pacodelaluna/add-dfc-provider-engine
Add DFC Provider engine
This commit is contained in:
1
Gemfile
1
Gemfile
@@ -11,6 +11,7 @@ gem 'rails_safe_tasks', '~> 1.0'
|
||||
gem "activerecord-import"
|
||||
|
||||
gem "catalog", path: "./engines/catalog"
|
||||
gem 'dfc_provider', path: './engines/dfc_provider'
|
||||
gem "order_management", path: "./engines/order_management"
|
||||
gem 'web', path: './engines/web'
|
||||
|
||||
|
||||
@@ -61,6 +61,13 @@ PATH
|
||||
specs:
|
||||
catalog (0.0.1)
|
||||
|
||||
PATH
|
||||
remote: engines/dfc_provider
|
||||
specs:
|
||||
dfc_provider (0.0.1)
|
||||
jwt (~> 2.2)
|
||||
rspec (~> 3.9)
|
||||
|
||||
PATH
|
||||
remote: engines/order_management
|
||||
specs:
|
||||
@@ -710,6 +717,7 @@ DEPENDENCIES
|
||||
delayed_job_web
|
||||
devise (~> 2.2.5)
|
||||
devise-encryptable (= 0.2.0)
|
||||
dfc_provider!
|
||||
diffy
|
||||
eventmachine (>= 1.2.3)
|
||||
factory_bot_rails
|
||||
|
||||
@@ -89,6 +89,9 @@ Openfoodnetwork::Application.routes.draw do
|
||||
|
||||
get 'sitemap.xml', to: 'sitemap#index', defaults: { format: 'xml' }
|
||||
|
||||
# Mount DFC API endpoints
|
||||
mount DfcProvider::Engine, at: '/'
|
||||
|
||||
# Mount Spree's routes
|
||||
mount Spree::Core::Engine, :at => '/'
|
||||
end
|
||||
|
||||
10
engines/dfc_provider/README.md
Normal file
10
engines/dfc_provider/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# DfcProvider
|
||||
|
||||
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.
|
||||
|
||||
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)
|
||||
|
||||
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`).
|
||||
@@ -0,0 +1,70 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Controller used to provide the API products for the DFC application
|
||||
module DfcProvider
|
||||
module Api
|
||||
class ProductsController < ::ActionController::Base
|
||||
# To access 'base_url' helper
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
before_filter :check_authorization,
|
||||
:check_user,
|
||||
:check_enterprise
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
products = @enterprise.
|
||||
inventory_variants.
|
||||
includes(:product, :inventory_items)
|
||||
|
||||
serialized_data = ::DfcProvider::ProductSerializer.
|
||||
new(products, base_url).
|
||||
serialized_data
|
||||
|
||||
render json: serialized_data
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_enterprise
|
||||
@enterprise =
|
||||
if params[:enterprise_id] == 'default'
|
||||
@user.enterprises.first
|
||||
else
|
||||
@user.enterprises.where(id: params[:enterprise_id]).first
|
||||
end
|
||||
|
||||
return if @enterprise.present?
|
||||
|
||||
head :not_found
|
||||
end
|
||||
|
||||
def check_authorization
|
||||
return if access_token.present?
|
||||
|
||||
head :unprocessable_entity
|
||||
end
|
||||
|
||||
def check_user
|
||||
@user = authorization_control.process
|
||||
|
||||
return if @user.present?
|
||||
|
||||
head :unauthorized
|
||||
end
|
||||
|
||||
def base_url
|
||||
"#{root_url}api/dfc_provider"
|
||||
end
|
||||
|
||||
def access_token
|
||||
request.headers['Authorization'].to_s.split(' ').last
|
||||
end
|
||||
|
||||
def authorization_control
|
||||
DfcProvider::AuthorizationControl.new(access_token)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Serializer used to render the products passed
|
||||
# into JSON-LD format based on DFC ontology
|
||||
module DfcProvider
|
||||
class ProductSerializer
|
||||
def initialize(products, base_url)
|
||||
@products = products
|
||||
@base_url = base_url
|
||||
end
|
||||
|
||||
def serialized_data
|
||||
{
|
||||
"@context" =>
|
||||
{
|
||||
"DFC" => "http://datafoodconsortium.org/ontologies/DFC_FullModel.owl#",
|
||||
"@base" => @base_url
|
||||
},
|
||||
"@id" => "/enterprise/products",
|
||||
"DFC:supplies" => serialized_products
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def serialized_products
|
||||
@products.map do |variant|
|
||||
{
|
||||
"DFC:description" => variant.name,
|
||||
"DFC:quantity" => variant.total_on_hand,
|
||||
"@id" => variant.id,
|
||||
"DFC:hasUnit" => { "@id" => "/unit/#{variant.unit_description.presence || 'piece'}" }
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Service used to authorize the user on DCF Provider API
|
||||
# It controls an OICD Access token and an enterprise.
|
||||
module DfcProvider
|
||||
class AuthorizationControl
|
||||
def initialize(access_token)
|
||||
@access_token = access_token
|
||||
end
|
||||
|
||||
def process
|
||||
decode_token
|
||||
find_ofn_user
|
||||
end
|
||||
|
||||
def decode_token
|
||||
data = JWT.decode(
|
||||
@access_token,
|
||||
nil,
|
||||
false
|
||||
)
|
||||
|
||||
@header = data.last
|
||||
@payload = data.first
|
||||
end
|
||||
|
||||
def find_ofn_user
|
||||
Spree::User.where(email: @payload['email']).first
|
||||
end
|
||||
end
|
||||
end
|
||||
11
engines/dfc_provider/config/routes.rb
Normal file
11
engines/dfc_provider/config/routes.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
DfcProvider::Engine.routes.draw do
|
||||
namespace :api do
|
||||
scope :dfc_provider, as: :dfc_provider, path: '/dfc_provider' do
|
||||
resources :enterprises, only: :none do
|
||||
resources :products, only: [:index]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
21
engines/dfc_provider/dfc_provider.gemspec
Normal file
21
engines/dfc_provider/dfc_provider.gemspec
Normal file
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
$LOAD_PATH.push File.expand_path('lib', __dir__)
|
||||
|
||||
# Maintain your gem's version:
|
||||
require "dfc_provider/version"
|
||||
|
||||
# Describe your gem and declare its dependencies:
|
||||
Gem::Specification.new do |spec|
|
||||
spec.name = 'dfc_provider'
|
||||
spec.version = DfcProvider::VERSION
|
||||
spec.authors = ["developers@ofn"]
|
||||
spec.summary = 'Provides an API stack implementing DFC semantic ' \
|
||||
'specifications'
|
||||
|
||||
spec.files = Dir["{app,config,lib}/**/*"] + ['README.md']
|
||||
spec.test_files = Dir['spec/**/*']
|
||||
|
||||
spec.add_dependency 'jwt', '~> 2.2'
|
||||
spec.add_dependency 'rspec', '~> 3.9'
|
||||
end
|
||||
6
engines/dfc_provider/lib/dfc_provider.rb
Normal file
6
engines/dfc_provider/lib/dfc_provider.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "dfc_provider/engine"
|
||||
|
||||
module DfcProvider
|
||||
end
|
||||
7
engines/dfc_provider/lib/dfc_provider/engine.rb
Normal file
7
engines/dfc_provider/lib/dfc_provider/engine.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DfcProvider
|
||||
class Engine < ::Rails::Engine
|
||||
isolate_namespace DfcProvider
|
||||
end
|
||||
end
|
||||
5
engines/dfc_provider/lib/dfc_provider/version.rb
Normal file
5
engines/dfc_provider/lib/dfc_provider/version.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DfcProvider
|
||||
VERSION = '0.0.1'
|
||||
end
|
||||
@@ -0,0 +1,99 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe DfcProvider::Api::ProductsController, type: :controller do
|
||||
render_views
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:enterprise) { create(:distributor_enterprise, owner: user) }
|
||||
let(:product) { create(:simple_product, supplier: enterprise ) }
|
||||
let!(:visible_inventory_item) do
|
||||
create(:inventory_item,
|
||||
enterprise: enterprise,
|
||||
variant: product.variants.first,
|
||||
visible: true)
|
||||
end
|
||||
|
||||
describe('.index') do
|
||||
context 'with authorization token' do
|
||||
before do
|
||||
request.env['Authorization'] = 'Bearer 123456.abcdef.123456'
|
||||
end
|
||||
|
||||
context 'with an authenticated user' do
|
||||
before do
|
||||
allow_any_instance_of(DfcProvider::AuthorizationControl)
|
||||
.to receive(:process)
|
||||
.and_return(user)
|
||||
end
|
||||
|
||||
context 'with an enterprise' do
|
||||
context 'given with an id' do
|
||||
context 'related to the user' do
|
||||
before { get :index, enterprise_id: 'default' }
|
||||
|
||||
it 'is successful' do
|
||||
expect(response.status).to eq 200
|
||||
end
|
||||
|
||||
it 'renders the related product' do
|
||||
expect(response.body)
|
||||
.to include(product.variants.first.name)
|
||||
end
|
||||
end
|
||||
|
||||
context 'not related to the user' do
|
||||
let(:enterprise) { create(:enterprise) }
|
||||
|
||||
it 'returns not_found head' do
|
||||
get :index, enterprise_id: enterprise.id
|
||||
expect(response.status).to eq 404
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'as default' do
|
||||
before { get :index, enterprise_id: 'default' }
|
||||
|
||||
it 'is successful' do
|
||||
expect(response.status).to eq 200
|
||||
end
|
||||
|
||||
it 'renders the related product' do
|
||||
expect(response.body)
|
||||
.to include(product.variants.first.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'without a recorded enterprise' do
|
||||
let(:enterprise) { create(:enterprise) }
|
||||
|
||||
it 'returns not_found head' do
|
||||
get :index, enterprise_id: 'default'
|
||||
expect(response.status).to eq 404
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'without an authenticated user' do
|
||||
it 'returns unauthorized head' do
|
||||
allow_any_instance_of(DfcProvider::AuthorizationControl)
|
||||
.to receive(:process)
|
||||
.and_return(nil)
|
||||
|
||||
get :index, enterprise_id: 'default'
|
||||
expect(response.status).to eq 401
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'without an authorization token' do
|
||||
it 'returns unprocessable_entity head' do
|
||||
get :index, enterprise_id: enterprise.id
|
||||
expect(response.status).to eq 422
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
5
engines/dfc_provider/spec/spec_helper.rb
Normal file
5
engines/dfc_provider/spec/spec_helper.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "../../spec/spec_helper.rb"
|
||||
|
||||
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].sort.each { |f| require f }
|
||||
Reference in New Issue
Block a user