-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #187 from samvera/ld_refactor_iri_template
Create models for IRI templates and use IRI template service to build URLs
- Loading branch information
Showing
11 changed files
with
652 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# Provide access to iri template configuration. | ||
# See https://www.hydra-cg.com/spec/latest/core/#templated-links for information on IRI Templated Links. | ||
# TODO: It would be good to find a more complete resource describing templated links. | ||
module Qa | ||
module IriTemplate | ||
class UrlConfig | ||
TYPE = "IriTemplate".freeze | ||
CONTEXT = "http://www.w3.org/ns/hydra/context.jsonld".freeze | ||
attr_reader :template # [String] the URL template with variables for substitution (required) | ||
attr_reader :variable_representation # [String] always "BasicRepresentation" # TODO what other values are supported and what do they mean | ||
attr_reader :mapping # [Array<Qa::IriTempalte::VariableMap>] array of maps for use with a template (required) | ||
|
||
# @param [Hash] url_config configuration hash for the iri template | ||
# @option url_config [String] :template the URL template with variables for substitution (required) | ||
# @option url_config [String] :variable_representation always "BasicRepresentation" # TODO what other values are supported and what do they mean | ||
# @option url_config [Array<Hash>] :mapping array of maps for use with a template (required) | ||
def initialize(url_config) | ||
@template = extract_template(config: url_config) | ||
@mapping = extract_mapping(config: url_config) | ||
@variable_representation = url_config.fetch(:variable_representation, 'BasicRepresentation') | ||
end | ||
|
||
# Selective extract substitution variable-value pairs from the provided substitutions. | ||
# @param [Hash, ActionController::Parameters] full set of passed in substitution values | ||
# @returns [HashWithIndifferentAccess] Only variable-value pairs for variables defined in the variable mapping. | ||
def extract_substitutions(substitutions) | ||
selected_substitutions = HashWithIndifferentAccess.new | ||
mapping.each do |m| | ||
selected_substitutions[m.variable] = substitutions[m.variable] if substitutions.key? m.variable | ||
end | ||
selected_substitutions | ||
end | ||
|
||
private | ||
|
||
# Extract the url template from the config | ||
# @param config [Hash] configuration holding the template to be extracted | ||
# @return [String] url template for accessing the authority | ||
def extract_template(config:) | ||
template = config.fetch(:template, nil) | ||
raise Qa::InvalidConfiguration, "template is required" unless template | ||
template | ||
end | ||
|
||
# Initialize the variable maps | ||
# @param config [Hash] configuration holding the variable maps to be extracted | ||
# @return [Array<IriTemplate::Map>] array of the variable maps | ||
def extract_mapping(config:) | ||
mapping = config.fetch(:mapping, nil) | ||
raise Qa::InvalidConfiguration, "mapping is required" unless mapping | ||
raise Qa::InvalidConfiguration, "mapping must include at least one map" if mapping.empty? | ||
mapping.collect { |m| Qa::IriTemplate::VariableMap.new(m) } | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
# Provide access to iri template variable map configuration. | ||
# See https://www.hydra-cg.com/spec/latest/core/#templated-links for information on IRI Templated Links - Variable Mapping. | ||
# TODO: It would be good to find a more complete resource describing templated links. | ||
module Qa | ||
module IriTemplate | ||
class VariableMap | ||
TYPE = "IriTemplateMapping".freeze | ||
attr_reader :variable | ||
attr_reader :default | ||
|
||
# @param [Hash] map configuration hash for the variable map | ||
# @option map [String] :variable name of the variable in the template (e.g. {?query} has the name 'query') | ||
# @option map [String] :property always "hydra:freetextQuery" # TODO what other values are supported and what do they mean | ||
# @option map [True | False] :required is this variable required | ||
# @option map [String] :default value to use if a value is not provided in the request | ||
def initialize(map) | ||
@variable = extract_variable(config: map) | ||
@required = extract_required(config: map) | ||
@default = extract_default(config: map) | ||
@property = map.fetch(:property, 'hydra:freetextQuery') | ||
end | ||
|
||
# Is this variable required? | ||
# @returns true if required; otherwise, false | ||
def required? | ||
@required | ||
end | ||
|
||
# TODO: When implementing more complex query substitution, simple_value is used when template url specifies variable as {var_name}. | ||
# Value to use in substitution, using default if one isn't passed in | ||
# @param [Object] value to use if it exists | ||
# @returns the value to use (e.g. 'fr') | ||
def simple_value(sub_value = nil) | ||
return sub_value.to_s if sub_value.present? | ||
raise Qa::IriTemplate::MissingParameter, "#{variable} is required, but missing" if required? | ||
default | ||
end | ||
|
||
# TODO: When implementing more complex query substitution, parameter_value is used when template url specifies variable as {?var_name}. | ||
# # Parameter and value to use in substitution, using default is one isn't passed in | ||
# # @param [Object] value to use if it exists | ||
# # @returns the parameter and value to use (e.g. 'language=fr') | ||
# def parameter_value(sub_value = nil) | ||
# simple_value = simple_value(sub_value) | ||
# return '' if simple_value.blank? | ||
# param_value = "#{variable}=#{simple_value}" | ||
# end | ||
|
||
private | ||
|
||
# Extract the variable name from the config | ||
# @param config [Hash] configuration (json) holding the variable map | ||
# @return [String] variable for substitution in the url tmeplate | ||
def extract_variable(config:) | ||
varname = config.fetch(:variable, nil) | ||
raise Qa::InvalidConfiguration, 'variable is required' unless varname | ||
varname | ||
end | ||
|
||
# Extract the variable name from the config | ||
# @param config [Hash] configuration (json) holding the variable map | ||
# @return [True | False] required as true or false | ||
def extract_required(config:) | ||
required = config.fetch(:required, nil) | ||
raise Qa::InvalidConfiguration, 'required must be true or false' unless required == true || required == false | ||
required | ||
end | ||
|
||
# Extract the default value from the config | ||
# @param config [Hash] configuration (json) holding the variable map | ||
# @return [String] default value to use for the variable; defaults to empty string | ||
def extract_default(config:) | ||
config.fetch(:default, '').to_s | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
# Provide service for building a URL based on an IRI Templated Link and its variable mappings based on provided substitutions. | ||
module Qa | ||
class IriTemplateService | ||
# Construct an url from an IriTemplate making identified substitutions | ||
# @param url_config [Qa::IriTemplate::UrlConfig] configuration (json) holding the template and variable mappings | ||
# @param substitutions [HashWithIndifferentAccess] name-value pairs to substitute into the url template | ||
# @return [String] url with substitutions | ||
def self.build_url(url_config:, substitutions:) | ||
# TODO: This is a very simple approach using direct substitution into the template string. | ||
# Better would be to... | ||
# * pattern {var_name} = simple value substitution in place of pattern produces 'value' | ||
# * pattern {?var_name} = parameter substitution in place of pattern produces 'var_name=value' | ||
# * patterns without a substitution are not included in the resulting URL | ||
# * appropriately adds '?' or '&' | ||
# * ensure proper escaping of values (e.g. value="A simple string" which is encoded as A%20simple%20string) | ||
# Even more advanced would be to... | ||
# * support BasicRepresentation (which is what it does now) | ||
# * support ExplicitRepresentation | ||
# * literal encoding for values (e.g. value="A simple string" becomes %22A%20simple%20string%22) | ||
# * language encoding for values (e.g. value="A simple string" becomes value="A simple string"@en which is encoded as %22A%20simple%20string%22%40en) | ||
# * type encoding for values (e.g. value=5.5 becomes value="5.5"^^http://www.w3.org/2001/XMLSchema#decimal which is encoded | ||
# as %225.5%22%5E%5Ehttp%3A%2F%2Fwww.w3.org%2F2001%2FXMLSchema%23decimal) | ||
# Fuller implementations parse the template into component parts and then build the URL by adding parts in as applicable. | ||
url = url_config.template | ||
url_config.mapping.each do |m| | ||
key = m.variable | ||
url = url.gsub("{?#{key}}", m.simple_value(substitutions[key])) # Incorrectly applies pattern {?var_name} to produce substitution 'value' | ||
# url.gsub("{#{key}}", m.simple_value(substitutions[key])) # TODO: pattern {var_name} should produce substitution 'value' | ||
# url.gsub("{?#{key}}", m.parameter_value(substitutions[key])) # TODO: pattern {?var_name} should produce substitution 'var_name=value' | ||
end | ||
url | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# Provide service for constructing the external access URL for an authority. | ||
module Qa | ||
module LinkedData | ||
class AuthorityUrlService | ||
# Build a url for an authority/subauthority for the specified action. | ||
# @param authority [Symbol] name of a registered authority | ||
# @param subauthority [String] name of a subauthority | ||
# @param action [Symbol] action with valid values :search or :term | ||
# @param action_request [String] the request the user is making of the authority (e.g. query text or term id/uri) | ||
# @param substitutions [Hash] variable-value pairs to substitute into the URL template | ||
# @returns a valid URL the submits the action request to the external authority | ||
def self.build_url(action_config:, action:, action_request:, substitutions: {}, subauthority: nil) | ||
action_validation(action) | ||
url_config = Qa::IriTemplate::UrlConfig.new(action_url(action_config, action)) | ||
selected_substitutions = url_config.extract_substitutions(substitutions) | ||
selected_substitutions[action_request_variable(action_config, action)] = action_request | ||
selected_substitutions[action_subauth_variable(action_config, action)] = action_subauth_variable_value(action_config, subauthority, action) if subauthority.present? | ||
|
||
Qa::IriTemplateService.build_url(url_config: url_config, substitutions: selected_substitutions) | ||
end | ||
|
||
def self.action_validation(action) | ||
return if [:search, :term].include? action | ||
raise Qa::UnsupportedAction, "#{action} Not Supported - Action must be one of the supported actions (e.g. :term, :search)" | ||
end | ||
private_class_method :action_validation | ||
|
||
# TODO: elr - rename search and term config methods to be the same to avoid all the ternary checks | ||
def self.action_url(auth_config, action) | ||
action == :search ? auth_config.url : auth_config.term_url | ||
end | ||
private_class_method :action_url | ||
|
||
def self.action_request_variable(action_config, action) | ||
key = action == :search ? :query : :term_id | ||
action == :search ? action_config.qa_replacement_patterns[key] : action_config.term_qa_replacement_patterns[key] | ||
end | ||
private_class_method :action_request_variable | ||
|
||
def self.action_subauth_variable(action_config, action) | ||
action == :search ? action_config.qa_replacement_patterns[:subauth] : action_config.term_qa_replacement_patterns[:subauth] | ||
end | ||
private_class_method :action_subauth_variable | ||
|
||
def self.action_subauth_variable_value(action_config, subauthority, action) | ||
case action | ||
when :search | ||
pattern = action_subauth_variable(action_config, action) | ||
default = action_config.url_mappings[pattern.to_sym][:default] | ||
action_config.subauthorities[subauthority.to_sym] || default | ||
when :term | ||
pattern = action_subauth_variable(action_config, action) | ||
default = action_config.term_url_mappings[pattern.to_sym][:default] | ||
action_config.term_subauthorities[subauthority.to_sym] || default | ||
end | ||
end | ||
private_class_method :action_subauth_variable_value | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
require 'spec_helper' | ||
|
||
RSpec.describe Qa::IriTemplate::UrlConfig do | ||
let(:url_template) do | ||
{ | ||
:"@context" => "http://www.w3.org/ns/hydra/context.jsonld", | ||
:"@type" => "IriTemplate", | ||
template: "http://localhost/test_default/search?subauth={?subauth}&query={?query}¶m1={?param1}¶m2={?param2}", | ||
variableRepresentation: "BasicRepresentation", | ||
mapping: [ | ||
{ | ||
:"@type" => "IriTemplateMapping", | ||
variable: "query", | ||
property: "hydra:freetextQuery", | ||
required: true | ||
}, | ||
{ | ||
:"@type" => "IriTemplateMapping", | ||
variable: "subauth", | ||
property: "hydra:freetextQuery", | ||
required: false, | ||
default: "search_sub1_name" | ||
}, | ||
{ | ||
:"@type" => "IriTemplateMapping", | ||
variable: "param1", | ||
property: "hydra:freetextQuery", | ||
required: false, | ||
default: "delta" | ||
}, | ||
{ | ||
:"@type" => "IriTemplateMapping", | ||
variable: "param2", | ||
property: "hydra:freetextQuery", | ||
required: false, | ||
default: "echo" | ||
} | ||
] | ||
} | ||
end | ||
|
||
describe 'model attributes' do | ||
subject { described_class.new(url_template) } | ||
|
||
it { is_expected.to respond_to :template } | ||
it { is_expected.to respond_to :variable_representation } | ||
it { is_expected.to respond_to :mapping } | ||
end | ||
|
||
describe '#initialize' do | ||
context 'when missing template' do | ||
before do | ||
allow(url_template).to receive(:fetch).with(:template, nil).and_return(nil) | ||
end | ||
|
||
it 'raises an error' do | ||
expect { described_class.new(url_template) }.to raise_error(Qa::InvalidConfiguration, 'template is required') | ||
end | ||
end | ||
|
||
context 'when missing mapping' do | ||
before do | ||
allow(url_template).to receive(:fetch).with(:template, nil).and_return("http://localhost/test_default/search?subauth={?subauth}&query={?query}¶m1={?param1}¶m2={?param2}") | ||
allow(url_template).to receive(:fetch).with(:mapping, nil).and_return(nil) | ||
end | ||
|
||
it 'raises an error' do | ||
expect { described_class.new(url_template) }.to raise_error(Qa::InvalidConfiguration, 'mapping is required') | ||
end | ||
end | ||
|
||
context 'when no maps defined' do | ||
before do | ||
allow(url_template).to receive(:fetch).with(:template, nil).and_return("http://localhost/test_default/search?subauth={?subauth}&query={?query}¶m1={?param1}¶m2={?param2}") | ||
allow(url_template).to receive(:fetch).with(:mapping, nil).and_return([]) | ||
end | ||
|
||
it 'raises an error' do | ||
expect { described_class.new(url_template) }.to raise_error(Qa::InvalidConfiguration, 'mapping must include at least one map') | ||
end | ||
end | ||
end | ||
|
||
describe '#template' do | ||
subject { described_class.new(url_template) } | ||
|
||
it 'returns the configured url template' do | ||
expect(subject.template).to eq 'http://localhost/test_default/search?subauth={?subauth}&query={?query}¶m1={?param1}¶m2={?param2}' | ||
end | ||
end | ||
|
||
describe '#mapping' do | ||
subject { described_class.new(url_template) } | ||
|
||
it 'returns an array of variable maps' do | ||
mapping = subject.mapping | ||
expect(mapping).to be_kind_of Array | ||
expect(mapping.size).to eq 4 | ||
expect(mapping.first).to be_kind_of Qa::IriTemplate::VariableMap | ||
end | ||
end | ||
end |
Oops, something went wrong.