Skip to content

Commit

Permalink
Introducing Qa::AuthorityWrapper
Browse files Browse the repository at this point in the history
Prior to this commit, the returned value from `Qa.authority_for` was
something that was inconsistent.  Depending on the controller (or authority), the `find`
and `search` would require different parameters.

With this commit, those nuances are encapsulated in their own methods.
This begins to work towards a more foundational promise of questioning
authority; namely that the end point that asked questions of QA didn't
need to know which type of authority it was.

That promise was partially delivered at the routes level but this commit
pushes that down to the internal API level.

As a bonus, and perhaps confusion, I've included the
`Qa::AuthorityRequestContext` as an interface to help convey how one
might provide a different (non-controller) context to instantiating and
authority.
  • Loading branch information
jeremyf committed Dec 7, 2022
1 parent 24776bc commit baf399f
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 12 deletions.
13 changes: 9 additions & 4 deletions app/controllers/qa/linked_data_terms_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def reload
# get "/search/linked_data/:vocab(/:subauthority)"
# @see Qa::Authorities::LinkedData::SearchQuery#search
def search # rubocop:disable Metrics/MethodLength
terms = @authority.search(query, request_header: request_header_service.search_header)
terms = @authority.search(query)
cors_allow_origin_header(response)
render json: terms
rescue Qa::ServiceUnavailable
Expand All @@ -65,7 +65,7 @@ def search # rubocop:disable Metrics/MethodLength
# get "/show/linked_data/:vocab/:subauthority/:id
# @see Qa::Authorities::LinkedData::FindTerm#find
def show # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
term = @authority.find(id, request_header: request_header_service.fetch_header)
term = @authority.find(id)
cors_allow_origin_header(response)
render json: term, content_type: request_header_service.content_type_for_format
rescue Qa::TermNotFound
Expand Down Expand Up @@ -95,7 +95,7 @@ def show # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
# get "/fetch/linked_data/:vocab"
# @see Qa::Authorities::LinkedData::FindTerm#find
def fetch # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
term = @authority.find(uri, request_header: request_header_service.fetch_header)
term = @authority.find(uri)
cors_allow_origin_header(response)
render json: term, content_type: request_header_service.content_type_for_format
rescue Qa::TermNotFound
Expand Down Expand Up @@ -157,8 +157,13 @@ def create_request_header_service
@request_header_service = request_header_service_class.new(request: request, params: params)
end

# @see Qa::AuthorityWrapper for these methods
delegate :search_header, :fetch_header, to: :request_header_service

def init_authority
@authority = Qa.authority_for(vocab: params[:vocab], subauthority: params[:subauthority])
@authority = Qa.authority_for(vocab: params[:vocab],
subauthority: params[:subauthority],
context: self)
rescue Qa::InvalidAuthorityError, Qa::InvalidLinkedDataAuthority => e
msg = e.message
logger.warn msg
Expand Down
3 changes: 2 additions & 1 deletion app/controllers/qa/terms_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ def init_authority # rubocop:disable Metrics/MethodLength
@authority = Qa.authority_for(vocab: params[:vocab],
subauthority: params[:subauthority],
# Included to preserve error message text
try_linked_data_config: false)
try_linked_data_config: false,
context: self)
rescue Qa::InvalidAuthorityError, Qa::InvalidSubAuthority, Qa::MissingSubAuthority => e
msg = e.message
logger.warn msg
Expand Down
22 changes: 15 additions & 7 deletions lib/qa.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require "qa/engine"
require "active_record"
require "activerecord-import"
require "qa/authority_wrapper"

module Qa
extend ActiveSupport::Autoload
Expand Down Expand Up @@ -80,7 +81,7 @@ class DataNormalizationError < StandardError; end
#
# @param vocab [String]
# @param subauthority [String]
#
# @param context [#params, #search_header, #fetch_header]
# @param try_linked_data_config [Boolean] when true attempt to check for a linked data authority;
# this is included as an option to help preserve error messaging from the 5.10.0 branch.
# Unless you want to mirror the error messages of `Qa::TermsController#init_authority` then
Expand All @@ -93,20 +94,27 @@ class DataNormalizationError < StandardError; end
# #fetch. This is provided as a means of normalizing how we initialize an authority.
# And to provide a means to request an authority both within a controller request cycle as
# well as outside of that cycle.
def self.authority_for(vocab:, subauthority: nil, try_linked_data_config: true)
def self.authority_for(vocab:, context:, subauthority: nil, try_linked_data_config: true)
authority = build_authority_for(vocab: vocab,
subauthority: subauthority,
try_linked_data_config: try_linked_data_config)
AuthorityWrapper.new(authority: authority, subauthority: subauthority, context: context)
end

# @api private
def self.build_authority_for(vocab:, subauthority: nil, try_linked_data_config: true)
authority_constant_name = "Qa::Authorities::#{vocab.to_s.camelcase}"
authority_constant = authority_constant_name.safe_constantize
if authority_constant.nil?
if try_linked_data_config
return Qa::Authorities::LinkedData::GenericAuthority.new(vocab.upcase.to_sym)
else
raise InvalidAuthorityError, authority_constant_name
end
return Qa::Authorities::LinkedData::GenericAuthority.new(vocab.upcase.to_sym) if try_linked_data_config

raise InvalidAuthorityError, authority_constant_name
end

return authority_constant.new if authority_constant.is_a?(Class)
return authority_constant.subauthority_for(subauthority) if subauthority.present?

raise Qa::MissingSubAuthority, "No sub-authority provided"
end
private_class_method :build_authority_for
end
3 changes: 3 additions & 0 deletions lib/qa/authorities/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,8 @@ def all
def find(_id)
raise NotImplementedError, "#{self.class}#find is unimplemented."
end

class_attribute :linked_data, instance_writer: false
self.linked_data = false
end
end
2 changes: 2 additions & 0 deletions lib/qa/authorities/linked_data/generic_authority.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class GenericAuthority < Base
attr_reader :authority_config
private :authority_config

self.linked_data = true

delegate :supports_term?, :term_subauthorities?, :term_subauthority?,
:term_id_expects_uri?, :term_id_expects_id?, to: :term_config

Expand Down
44 changes: 44 additions & 0 deletions lib/qa/authority_request_context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
module Qa
# @note THIS IS NOT TESTED NOR EXERCISED CODE IT IS PROVIDED AS CONJECTURE. FUTURE CHANGES MIGHT
# BUILD AND REFACTOR UPON THIS.
#
# @api private
# @abstract
#
# This class is responsible for exposing methods that are required by both linked data and
# non-linked data authorities. As of v5.10.0, those three methods are: params, search_header,
# fetch_header. Those are the methods that are used in {Qa::LinkedData::RequestHeaderService} and
# in {Qa::Authorities::Discogs::GenericAuthority}.
#
# The intention is to provide a class that can behave like a controller object without being that
# entire controller object.
#
# @see Qa::LinkedData::RequestHeaderService
# @see Qa::Authorities::Discogs::GenericAuthority
class AuthorityRequestContext
def self.fallback
new
end

def initialize(params: {}, **kwargs)
@params = params
(SEARCH_HEADER_KEYS + FETCH_HEADER_KEYS).uniq.each do |key|
send("#{key}=", kwargs[key]) if kwargs.key?(key)
end
end

SEARCH_HEADER_KEYS = %i[request request_id subauthority user_language performance_data context response_header replacements].freeze
FETCH_HEADER_KEYS = %i[request request_id subauthority user_language performance_data format response_header replacements].freeze

attr_accessor :params
attr_accessor(*(SEARCH_HEADER_KEYS + FETCH_HEADER_KEYS).uniq)

def search_header
@headers.slice(*SEARCH_HEADER_KEYS).compact
end

def fetch_header
@headers.slice(*FETCH_HEADER_KEYS).compact
end
end
end
62 changes: 62 additions & 0 deletions lib/qa/authority_wrapper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
module Qa
# @api public
# @since v5.11.0
#
# The intention of this wrapper is to provide a common interface that both linked and non-linked
# data can use. There are implementation differences between the two, but with this wrapper, the
# goal is to draw attention to those differences and insulate the end user from those issues.
#
# One benefit in introducing this class is that when interacting with a questioning authority
# implementation you don't need to consider "Hey when I instantiate an authority, is this linked
# data or not?" And what specifically are the parameter differences. You will need to perhaps
# include some additional values in the context if you don't call this from a controller.
class AuthorityWrapper
# @param authority [#find, #search]
# @param subauthority [#to_s]
# @param context [#params, #search_header, #fetch_header]
def initialize(authority:, subauthority:, context:)
@authority = authority
@subauthority = subauthority
@context = context
configure!
end
attr_reader :authority, :context, :subauthority

def search(value)
if linked_data?
# should respond to search_header
authority.search(value, request_header: context.search_header)
elsif authority.method(:search).arity == 2
# This context should respond to params; see lib/qa/authorities/discogs/generic_authority.rb
authority.search(value, context)
else
authority.search(value)
end
end

# context has params
def find(value)
if linked_data?
# should respond to fetch_header
authority.find(value, request_header: context.fetch_header)
elsif authority.method(:find).arity == 2
authority.find(value, context)
else
authority.find(value)
end
end
alias fetch find

def method_missing(method_name, *arguments, &block)
authority.send(method_name, *arguments, &block)
end

def respond_to_missing?(method_name, include_private = false)
authority.respond_to?(method_name, include_private)
end

def configure!
@context.subauthority = @subauthority if @context.respond_to?(:subauthority)
end
end
end

0 comments on commit baf399f

Please sign in to comment.