Merge pull request #5231 from pacodelaluna/add-dfc-provider-engine

Add DFC Provider engine
This commit is contained in:
Luis Ramos
2020-05-11 20:27:21 +01:00
committed by GitHub
14 changed files with 314 additions and 0 deletions

View File

@@ -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'

View File

@@ -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

View File

@@ -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

View 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`).

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View 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

View File

@@ -0,0 +1,6 @@
# frozen_string_literal: true
require "dfc_provider/engine"
module DfcProvider
end

View File

@@ -0,0 +1,7 @@
# frozen_string_literal: true
module DfcProvider
class Engine < ::Rails::Engine
isolate_namespace DfcProvider
end
end

View File

@@ -0,0 +1,5 @@
# frozen_string_literal: true
module DfcProvider
VERSION = '0.0.1'
end

View File

@@ -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

View 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 }