Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extracting Qa.authority_for #379

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 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,9 +157,14 @@ 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::Authorities::LinkedData::GenericAuthority.new(vocab_param)
rescue Qa::InvalidLinkedDataAuthority => e
@authority = Qa.authority_for(vocab: params[:vocab],
subauthority: params[:subauthority],
context: self)
rescue Qa::InvalidAuthorityError, Qa::InvalidLinkedDataAuthority => e
msg = e.message
logger.warn msg
render json: { errors: msg }, status: :bad_request
Expand Down
29 changes: 9 additions & 20 deletions app/controllers/qa/terms_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,26 +69,15 @@ def check_vocab_param
end

def init_authority # rubocop:disable Metrics/MethodLength
begin
mod = authority_class.camelize.constantize
rescue NameError
msg = "Unable to initialize authority #{authority_class}"
logger.warn msg
render json: { errors: msg }, status: :bad_request
return
end
begin
@authority = if mod.is_a? Class
mod.new
else
raise Qa::MissingSubAuthority, "No sub-authority provided" if params[:subauthority].blank?
mod.subauthority_for(params[:subauthority])
end
rescue Qa::InvalidSubAuthority, Qa::MissingSubAuthority => e
msg = e.message
logger.warn msg
render json: { errors: msg }, status: :bad_request
end
@authority = Qa.authority_for(vocab: params[:vocab],
subauthority: params[:subauthority],
# Included to preserve error message text
try_linked_data_config: false,
context: self)
rescue Qa::InvalidAuthorityError, Qa::InvalidSubAuthority, Qa::MissingSubAuthority => e
msg = e.message
logger.warn msg
render json: { errors: msg }, status: :bad_request
end

def check_query_param
Expand Down
50 changes: 50 additions & 0 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 @@ -30,6 +31,13 @@ def self.deprecation_warning(in_msg: nil, msg:)
warn "[DEPRECATED] #{in_msg}#{msg} It will be removed in the next major release."
end

# Raised when the authority is not valid
class InvalidAuthorityError < RuntimeError
def initialize(authority_class)
super "Unable to initialize authority #{authority_class}"
end
end

# Raised when the configuration directory for local authorities doesn't exist
class ConfigDirectoryNotFound < StandardError; end

Expand Down Expand Up @@ -67,4 +75,46 @@ class MissingParameter < StandardError; end

# Raised when data is returned but cannot be normalized
class DataNormalizationError < StandardError; end

# @api public
# @since 5.11.0
#
# @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
# use the default value.
#
# @note :try_linked_data_config is included to preserve error message text; something which is
# extensively tested in this gem.
#
# @return [#search, #find] an authority that will respond to #search and #find; and in some cases
# #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:, 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?
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
3 changes: 2 additions & 1 deletion lib/qa/authorities/loc/generic_authority.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ def find(id)
end

def find_url(id)
"https://id.loc.gov/authorities/#{@subauthority}/#{id}.json"
root_fetch_slug = Loc.root_fetch_slug_for(@subauthority)
File.join("https://id.loc.gov/", root_fetch_slug, "/#{@subauthority}/#{id}.json")
end

private
Expand Down
18 changes: 18 additions & 0 deletions lib/qa/authorities/loc_subauthority.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module Qa::Authorities::LocSubauthority
# @todo Rename to reflect that this is a URI encoded url fragement used only for searching.
def get_url_for_authority(authority)
if authorities.include?(authority) then authority_base_url
elsif vocabularies.include?(authority) then vocab_base_url
Expand All @@ -7,6 +8,23 @@ def get_url_for_authority(authority)
end
end

# @note The returned value is the root directory of the URL. The graphicMaterials sub-authority
# has a "type" of vocabulary. https://id.loc.gov/vocabulary/graphicMaterials/tgm008083.html
# In some cases, this is plural and in others this is singular.
#
# @param authority [String] the LOC authority that matches one of the types
# @return [String]
#
# @note there is a relationship between the returned value and the encoded URLs returned by
# {#get_url_for_authority}.
def root_fetch_slug_for(authority)
validate_subauthority!(authority)
return "authorities" if authorities.include?(authority)
return "vocabulary" if vocabularies.include?(authority)
return "datatype" if datatypes.include?(authority)
return "vocabulary/preservation" if preservation.include?(authority)
end

def authorities
[
"subjects",
Expand Down
49 changes: 49 additions & 0 deletions lib/qa/authority_request_context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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: {}, headers: {}, **kwargs)
@params = params
@headers = headers
(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, :headers
attr_accessor(*(SEARCH_HEADER_KEYS + FETCH_HEADER_KEYS).uniq)

def search_header
SEARCH_HEADER_KEYS.each_with_object(headers.deep_dup) do |key, header|
header[key] = send(key) if send(key).present?
end.compact
end

def fetch_header
FETCH_HEADER_KEYS.each_with_object(headers.deep_dup) do |key, header|
header[key] = send(key) if send(key).present?
end.compact
end
end
end
63 changes: 63 additions & 0 deletions lib/qa/authority_wrapper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
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
require 'qa/authority_request_context.rb'
# @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
12 changes: 12 additions & 0 deletions spec/lib/authorities/loc_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@
end
end

describe ".root_fetch_slug_for" do
it "raises an error for an invalid subauthority" do
expect do
described_class.root_fetch_slug_for("no-one-would-ever-have-this-one")
end.to raise_error Qa::InvalidSubAuthority
end

it "returns the corresponding type for the given subauthority" do
expect(described_class.root_fetch_slug_for("graphicMaterials")).to eq("vocabulary")
end
end

describe "#response" do
subject { authority.response(url) }
let :authority do
Expand Down