Skip to content

Commit

Permalink
extract out ldpath processing into ldpath service
Browse files Browse the repository at this point in the history
  • Loading branch information
elrayle committed Apr 19, 2019
1 parent b866dfc commit 1015de0
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 57 deletions.
31 changes: 6 additions & 25 deletions app/models/qa/linked_data/config/context_property_map.rb
Expand Up @@ -67,7 +67,7 @@ def expand_uri?
# Values of this property for a specfic subject URI
# @return [Array<String>] values for this property
def values(graph, subject_uri)
ldpath_evaluate(basic_program, graph, subject_uri)
Qa::LinkedData::LdpathService.ldpath_evaluate(program: basic_program, graph: graph, subject_uri: subject_uri)
end

# Values of this property for a specfic subject URI with URI values expanded to include id and label.
Expand Down Expand Up @@ -98,44 +98,25 @@ def extract_label
end

def basic_program
@basic_program ||= ldpath_program(ldpath)
@basic_program ||= Qa::LinkedData::LdpathService.ldpath_program(ldpath: ldpath, prefixes: prefixes)
end

def expansion_label_program
@expansion_label_program ||= ldpath_program(expansion_label_ldpath)
@expansion_label_program ||= Qa::LinkedData::LdpathService.ldpath_program(ldpath: expansion_label_ldpath, prefixes: prefixes)
end

def expansion_id_program
@expansion_id_program ||= ldpath_program(expansion_id_ldpath)
end

def ldpath_program(ldpath)
program_code = ""
prefixes.each { |key, url| program_code << "@prefix #{key} : <#{url}> \;\n" }
program_code << "property = #{ldpath} \;"
Ldpath::Program.parse program_code
rescue => e
Rails.logger.warn("WARNING: #{I18n.t('qa.linked_data.ldpath.parse_logger_error')} (ldpath='#{ldpath}')\n cause: #{e.message}")
raise StandardError, I18n.t('qa.linked_data.ldpath.parse_error')
end

def ldpath_evaluate(program, graph, subject_uri)
return VALUE_ON_ERROR if program.blank?
output = program.evaluate subject_uri, graph
output.present? ? output['property'].uniq : nil
rescue => e
Rails.logger.warn("WARNING: #{I18n.t('qa.linked_data.ldpath.evaluate_logger_error')} (ldpath='#{ldpath}')\n cause: #{e.message}")
raise StandardError, I18n.t('qa.linked_data.ldpath.evaluate_error')
@expansion_id_program ||= Qa::LinkedData::LdpathService.ldpath_program(ldpath: expansion_id_ldpath, prefixes: prefixes)
end

def expansion_label(graph, uri)
label = ldpath_evaluate(expansion_label_program, graph, RDF::URI(uri))
label = Qa::LinkedData::LdpathService.ldpath_evaluate(program: expansion_label_program, graph: graph, subject_uri: RDF::URI(uri))
label.size == 1 ? label.first : label
end

def expansion_id(graph, uri)
return uri if expansion_id_ldpath.blank?
id = ldpath_evaluate(expansion_id_program, graph, RDF::URI(uri))
id = Qa::LinkedData::LdpathService.ldpath_evaluate(program: expansion_id_program, graph: graph, subject_uri: RDF::URI(uri))
id.size == 1 ? id.first : id
end
end
Expand Down
40 changes: 40 additions & 0 deletions app/services/qa/linked_data/ldpath_service.rb
@@ -0,0 +1,40 @@
# Defines the external authority predicates used to extract additional context from the graph.
require 'ldpath'

module Qa
module LinkedData
class LdpathService
VALUE_ON_ERROR = [].freeze

# Create the ldpath program for a given ldpath.
# @param ldpath [String] ldpath to follow to get a value from a graph (documation: http://marmotta.apache.org/ldpath/language.html)
# @param prefixes [Hash] shortcut names for URI prefixes with key = part of predicate that is the same for all terms (e.g. { "madsrdf": "http://www.loc.gov/mads/rdf/v1#" })
# @return [Ldpath::Program] an executable program that will extract a value from a graph
def self.ldpath_program(ldpath:, prefixes: {})
program_code = ""
prefixes.each { |key, url| program_code << "@prefix #{key} : <#{url}> \;\n" }
program_code << "property = #{ldpath} \;"
Ldpath::Program.parse program_code
rescue => e
Rails.logger.warn("WARNING: #{I18n.t('qa.linked_data.ldpath.parse_logger_error')}... cause: #{e.message}\n ldpath_program=\n#{program_code}")
raise StandardError, I18n.t("qa.linked_data.ldpath.parse_error") + "... cause: #{e.message}"
end

# Evaluate an ldpath for a specific subject uri in the context of a graph and return the extracted values.
# @param program [Ldpath::Program] an executable program that will extract a value from a graph
# @param graph [RDF::Graph] the graph from which the values will be extracted
# @param subject_uri [RDF::URI] retrieved values will be limited to those with the subject uri
# @param limit_to_context [Boolean] if true, the evaluation process will not make any outside network calls.
# It will limit results to those found in the context graph.
## @return [Array<String>] the extracted values based on the ldpath
def self.ldpath_evaluate(program:, graph:, subject_uri:, limit_to_context: Qa.config.limit_ldpath_to_context?)
return VALUE_ON_ERROR if program.blank?
output = program.evaluate(subject_uri, context: graph, limit_to_context: limit_to_context)
output.present? ? output['property'].uniq : nil
rescue => e
Rails.logger.warn("WARNING: #{I18n.t('qa.linked_data.ldpath.evaluate_logger_error')} (cause: #{e.message}")
raise StandardError, I18n.t("qa.linked_data.ldpath.evaluate_error") + "... cause: #{e.message}"
end
end
end
end
4 changes: 4 additions & 0 deletions lib/generators/qa/install/templates/config/initializers/qa.rb
Expand Up @@ -14,4 +14,8 @@
# For linked data access, specify default language for sorting and selection. The default is only used if a language is not
# specified in the authority's configuration file and not passed in as a parameter. (e.g. :en, [:en], or [:en, :fr])
# config.default_language = :en

# When true, prevents ldpath requests from making additional network calls. All values will come from the context graph
# passed to the ldpath request.
# config.limit_ldpath_to_context = true
end
6 changes: 6 additions & 0 deletions lib/qa.rb
Expand Up @@ -24,6 +24,12 @@ def self.config(&block)
@config
end

def self.deprecation_warning(in_msg: nil, msg:)
return if Rails.env == 'test'
in_msg = in_msg.present? ? "In #{in_msg}, " : ''
warn "[DEPRECATED] #{in_msg}#{msg} It will be removed in the next major release."
end

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

Expand Down
2 changes: 1 addition & 1 deletion lib/qa/authorities/linked_data/config.rb
Expand Up @@ -79,7 +79,7 @@ def convert_1_0_to_2_0
def convert_1_0_url_to_2_0_url(action_key)
url_template = @authority_config.fetch(action_key, {}).fetch(:url, {}).fetch(:template, "")
return if url_template.blank?
warn "[DEPRECATED] #Linked data configuration #{authority_name} has 1.0 version format which is deprecated; update to version 2.0 configuration."
Qa.deprecation_warning(msg: "Linked data configuration #{authority_name} has 1.0 version format which is deprecated; update to version 2.0 configuration.")
@authority_config[action_key][:url][:template] = url_template.gsub("{?", "{")
end
end
Expand Down
8 changes: 8 additions & 0 deletions lib/qa/configuration.rb
Expand Up @@ -37,5 +37,13 @@ def valid_authority_reload_token?(token)
def default_language
@default_language ||= :en
end

# When true, prevents ldpath requests from making additional network calls. All values will come from the context graph
# passed to the ldpath request.
attr_writer :limit_ldpath_to_context
def limit_ldpath_to_context?
return true if @limit_ldpath_to_context.nil?
@limit_ldpath_to_context
end
end
end
18 changes: 18 additions & 0 deletions spec/lib/configuration_spec.rb
Expand Up @@ -68,4 +68,22 @@
end
end
end

describe '#limit_ldpath_to_context' do
context 'when NOT configured' do
it 'returns true' do
expect(subject.limit_ldpath_to_context?).to be true
end
end

context 'when configured' do
before do
subject.limit_ldpath_to_context = false
end

it 'returns the configured value' do
expect(subject.limit_ldpath_to_context?).to be false
end
end
end
end
34 changes: 3 additions & 31 deletions spec/models/linked_data/config/context_property_map_spec.rb
Expand Up @@ -209,34 +209,6 @@
it 'returns the values selected from the graph' do
expect(subject.values(graph, subject_uri)).to match_array coordinates
end

context 'when ldpath_program gets parse error' do
let(:ldpath) { property_map[:ldpath] }
let(:cause) { "undefined method `ascii_tree' for nil:NilClass" }
let(:warning) { I18n.t('qa.linked_data.ldpath.parse_logger_error') }
let(:log_message) { "WARNING: #{warning} (ldpath='#{ldpath}')\n cause: #{cause}" }

before { allow(Ldpath::Program).to receive(:parse).with(anything).and_raise(cause) }

it 'logs error and returns PARSE ERROR as the value' do
expect(Rails.logger).to receive(:warn).with(log_message)
expect { subject.values(graph, subject_uri) }.to raise_error StandardError, I18n.t('qa.linked_data.ldpath.parse_error')
end
end

context 'when ldpath_evaluate gets parse error' do
let(:ldpath) { property_map[:ldpath] }
let(:cause) { "unknown cause" }
let(:warning) { I18n.t('qa.linked_data.ldpath.evaluate_logger_error') }
let(:log_message) { "WARNING: #{warning} (ldpath='#{ldpath}')\n cause: #{cause}" }

before { allow(program).to receive(:evaluate).with(subject_uri, graph).and_raise(cause) }

it 'logs error and returns PARSE ERROR as the value' do
expect(Rails.logger).to receive(:warn).with(log_message)
expect { subject.values(graph, subject_uri) }.to raise_error StandardError, I18n.t('qa.linked_data.ldpath.evaluate_error')
end
end
end

describe '#expand_uri?' do
Expand Down Expand Up @@ -271,9 +243,9 @@
allow(Ldpath::Program).to receive(:parse).with('property = madsrdf:identifiesRWO/madsrdf:birthDate/schema:label ;').and_return(basic_program)
allow(Ldpath::Program).to receive(:parse).with('property = skos:prefLabel ::xsd:string ;').and_return(expanded_label_program)
allow(Ldpath::Program).to receive(:parse).with('property = loc:lccn ::xsd:string ;').and_return(expanded_id_program)
allow(basic_program).to receive(:evaluate).with(subject_uri, graph).and_return('property' => [expanded_uri])
allow(expanded_label_program).to receive(:evaluate).with(RDF::URI.new(subject_uri), graph).and_return('property' => [expanded_label])
allow(expanded_id_program).to receive(:evaluate).with(RDF::URI.new(subject_uri), graph).and_return('property' => [expanded_id])
allow(basic_program).to receive(:evaluate).with(subject_uri, context: graph, limit_to_context: true).and_return('property' => [expanded_uri])
allow(expanded_label_program).to receive(:evaluate).with(RDF::URI.new(subject_uri), context: graph, limit_to_context: true).and_return('property' => [expanded_label])
allow(expanded_id_program).to receive(:evaluate).with(RDF::URI.new(subject_uri), context: graph, limit_to_context: true).and_return('property' => [expanded_id])
end
it 'returns the uri, id, label for the expanded uri value' do
expanded_values = subject.expanded_values(graph, subject_uri).first
Expand Down
61 changes: 61 additions & 0 deletions spec/services/linked_data/ldpath_service_spec.rb
@@ -0,0 +1,61 @@
require 'spec_helper'

RSpec.describe Qa::LinkedData::LdpathService do
let(:ldpath) { 'skos:prefLabel ::xsd:string' }

describe '.ldpath_program' do
subject { described_class.ldpath_program(ldpath: ldpath, prefixes: prefixes) }

let(:prefixes) do
{ skos: 'http://www.w3.org/2004/02/skos/core#' }
end

it 'returns instance of Ldpath::Program' do
expect(subject).to be_kind_of Ldpath::Program
end

context 'when ldpath_program gets parse error' do
let(:cause) { "undefined method `ascii_tree' for nil:NilClass" }
let(:warning) { I18n.t('qa.linked_data.ldpath.parse_logger_error') }
let(:program_code) { "@prefix skos : <http://www.w3.org/2004/02/skos/core#> ;\nproperty = skos:prefLabel ::xsd:string ;" }
let(:log_message) { "WARNING: #{warning}... cause: #{cause}\n ldpath_program=\n#{program_code}" }

before { allow(Ldpath::Program).to receive(:parse).with(anything).and_raise(cause) }

it 'logs error and returns PARSE ERROR as the value' do
expect(Rails.logger).to receive(:warn).with(log_message)
expect { subject.values(graph, subject_uri) }.to raise_error StandardError, I18n.t('qa.linked_data.ldpath.parse_error') + "... cause: #{cause}"
end
end
end

describe '.ldpath_evaluate' do
subject { described_class.ldpath_evaluate(program: program, graph: graph, subject_uri: subject_uri) }

let(:program) { instance_double(Ldpath::Program) }
let(:graph) { instance_double(RDF::Graph) }
let(:subject_uri) { instance_double(RDF::URI) }
let(:values) { ['Expanded Label'] }

before do
allow(Ldpath::Program).to receive(:parse).with('property = skos:prefLabel ::xsd:string ;').and_return(program)
allow(program).to receive(:evaluate).with(subject_uri, context: graph, limit_to_context: true).and_return('property' => values)
end
it 'returns the extracted label' do
expect(subject).to match_array values
end

context 'when ldpath_evaluate gets parse error' do
let(:cause) { "unknown cause" }
let(:warning) { I18n.t('qa.linked_data.ldpath.evaluate_logger_error') }
let(:log_message) { "WARNING: #{warning} (cause: #{cause}" }

before { allow(program).to receive(:evaluate).with(subject_uri, context: graph, limit_to_context: true).and_raise(cause) }

it 'logs error and returns PARSE ERROR as the value' do
expect(Rails.logger).to receive(:warn).with(log_message)
expect { subject.values(graph, subject_uri) }.to raise_error StandardError, I18n.t('qa.linked_data.ldpath.evaluate_error') + "... cause: #{cause}"
end
end
end
end

0 comments on commit 1015de0

Please sign in to comment.