Skip to content

Commit

Permalink
Merge 43c689d into abf506f
Browse files Browse the repository at this point in the history
  • Loading branch information
elrayle committed Feb 13, 2019
2 parents abf506f + 43c689d commit c956715
Show file tree
Hide file tree
Showing 7 changed files with 270 additions and 8 deletions.
11 changes: 8 additions & 3 deletions app/controllers/qa/linked_data_terms_controller.rb
Expand Up @@ -35,7 +35,7 @@ def reload
# get "/search/linked_data/:vocab(/:subauthority)"
# @see Qa::Authorities::LinkedData::SearchQuery#search
def search
terms = @authority.search(query, subauth: subauthority, language: language, replacements: replacement_params)
terms = @authority.search(query, subauth: subauthority, language: language, replacements: replacement_params, context: context?)
cors_allow_origin_header(response)
render json: terms
rescue Qa::ServiceUnavailable
Expand Down Expand Up @@ -176,7 +176,7 @@ def replacement_params
end

def subauth_warn_msg
subauthority.nil? ? "" : " sub-authority #{subauthority} in"
subauthority.blank? ? "" : " sub-authority #{subauthority} in"
end

def format
Expand All @@ -186,7 +186,12 @@ def format
end

def jsonld?
format == 'jsonld'
format.casecmp('jsonld').zero?
end

def context?
context = params.fetch(:context, 'false')
context.casecmp('true').zero?
end

def validate_auth_reload_token
Expand Down
46 changes: 46 additions & 0 deletions app/services/qa/linked_data/mapper/context_mapper_service.rb
@@ -0,0 +1,46 @@
# Provide service for mapping predicates to object values.
module Qa
module LinkedData
module Mapper
class ContextMapperService
class_attribute :graph_service
self.graph_service = Qa::LinkedData::GraphService

class << self
# Extract predicates specified in the predicate_map from the graph and return as a value map for a single subject URI.
# @param graph [RDF::Graph] the graph from which to extract result values
# @param context_map [Qa::LinkedData::Config::ContextMap] defines properties to extract from the graph to provide additional context
# @param subject_uri [RDF::URI] the subject within the graph for which the values are being extracted
# @return [<Hash<Symbol><Array<Object>>] mapped context values and information with hash of map key = array of object values for predicates identified in predicate_map.
# @example returned context map with one property defined
# [{"group" => "group label,
# "property" => "property label",
# "values" => ["value 1","value 2"],
# "selectable" => true,
# "drillable" => false}]
def map_context(graph:, context_map:, subject_uri:)
context = []
context_map.properties.each do |property_map|
values = property_map.values(graph, subject_uri)
next if values.blank?
context << construct_context(context_map, property_map, values)
end
context
end

private

def construct_context(context_map, property_map, values)
property_info = {}
property_info["group"] = context_map.group_label(property_map.group_id) if property_map.group? # TODO: should be group label
property_info["property"] = property_map.label
property_info["values"] = values
property_info["selectable"] = property_map.selectable?
property_info["drillable"] = property_map.drillable?
property_info
end
end
end
end
end
end
Expand Up @@ -3,9 +3,10 @@ module Qa
module LinkedData
module Mapper
class SearchResultsMapperService
class_attribute :graph_mapper_service, :deep_sort_service
class_attribute :graph_mapper_service, :deep_sort_service, :context_mapper_service
self.graph_mapper_service = Qa::LinkedData::Mapper::GraphMapperService
self.deep_sort_service = Qa::LinkedData::DeepSortService
self.context_mapper_service = Qa::LinkedData::Mapper::ContextMapperService

class << self
# Extract predicates specified in the predicate_map from the graph and return as an array of value maps for each search result subject URI.
Expand All @@ -21,6 +22,7 @@ class << self
# sort: 'http://vivoweb.org/ontology/core#rank'
# }
# @param sort_key [Symbol] the key in the predicate map for the value on which to sort
# @param context_map [Qa::LinkedData::Config::ContextMap] map of additional context to include in the results
# @return [Array<Hash<Symbol><Array<Object>>>] mapped result values with each result as an element in the array
# with hash of map key = array of object values for predicates identified in map parameter.
# @example value map for a single result
Expand All @@ -31,11 +33,17 @@ class << self
# :altlabel=>[],
# :sort=>[#<RDF::Literal:0x3fcff54b4c18("2")>]}
# ]
def map_values(graph:, predicate_map:, sort_key:, preferred_language: nil)
def map_values(graph:, predicate_map:, sort_key:, preferred_language: nil, context_map: nil)
search_matches = []
graph.subjects.each do |subject|
next if subject.anonymous? # skip blank nodes
values = graph_mapper_service.map_values(graph: graph, predicate_map: predicate_map, subject_uri: subject)
values = graph_mapper_service.map_values(graph: graph, predicate_map: predicate_map, subject_uri: subject) do |value_map|
next value_map if context_map.blank?
context = {}
context = context_mapper_service.map_context(graph: graph, context_map: context_map, subject_uri: subject) if context_map.present?
value_map[:context] = context
value_map
end
search_matches << values unless sort_key.present? && values[sort_key].blank?
end
search_matches = deep_sort_service.new(search_matches, sort_key, preferred_language).sort
Expand Down
15 changes: 13 additions & 2 deletions lib/qa/authorities/linked_data/search_query.rb
Expand Up @@ -25,13 +25,15 @@ def initialize(search_config)
# @param language [Symbol] (optional) language used to select literals when multi-language is supported (e.g. :en, :fr, etc.)
# @param replacements [Hash] (optional) replacement values with { pattern_name (defined in YAML config) => value }
# @param subauth [String] (optional) the subauthority to query
# @param context [Boolean] (optional) true if context should be returned with the results; otherwise, false (default: false)
# @return [String] json results
# @example Json Results for Linked Data Search
# [ {"uri":"http://id.worldcat.org/fast/5140","id":"5140","label":"Cornell, Joseph"},
# {"uri":"http://id.worldcat.org/fast/72456","id":"72456","label":"Cornell, Sarah Maria, 1802-1832"},
# {"uri":"http://id.worldcat.org/fast/409667","id":"409667","label":"Cornell, Ezra, 1807-1874"} ]
def search(query, language: nil, replacements: {}, subauth: nil)
def search(query, language: nil, replacements: {}, subauth: nil, context: false)
raise Qa::InvalidLinkedDataAuthority, "Unable to initialize linked data search sub-authority #{subauth}" unless subauth.nil? || subauthority?(subauth)
@context = context
@language = language_service.preferred_language(user_language: language, authority_language: search_config.language)
url = authority_service.build_url(action_config: search_config, action: :search, action_request: query, substitutions: replacements, subauthority: subauth)
Rails.logger.info "QA Linked Data search url: #{url}"
Expand All @@ -48,10 +50,18 @@ def load_graph(url:)

def parse_search_authority_response
results = results_mapper_service.map_values(graph: @graph, predicate_map: preds_for_search, sort_key: :sort,
preferred_language: @language)
preferred_language: @language, context_map: context_map)
convert_results_to_json(results)
end

def context_map
context? ? search_config.context_map : nil
end

def context?
@context == true
end

def preds_for_search
label_pred_uri = search_config.results_label_predicate
raise Qa::InvalidConfiguration, "required label_predicate is missing in search configuration for LOD authority #{auth_name}" if label_pred_uri.nil?
Expand All @@ -78,6 +88,7 @@ def convert_result_to_json(result)
json_result[:uri] = result[:uri].first.to_s
json_result[:id] = result[:id].first.to_s
json_result[:label] = full_label(result[:label], result[:altlabel])
json_result[:context] = result[:context] if context?
json_result
end

Expand Down
23 changes: 23 additions & 0 deletions spec/controllers/linked_data_terms_controller_spec.rb
Expand Up @@ -231,6 +231,29 @@
end
end
end

context 'when processing context' do
before do
Qa.config.disable_cors_headers
stub_request(:get, 'http://experimental.worldcat.org/fast/search?maximumRecords=3&query=cql.any%20all%20%22cornell%22&sortKeys=usage')
.to_return(status: 200, body: webmock_fixture('lod_oclc_all_query_3_results.rdf.xml'), headers: { 'Content-Type' => 'application/rdf+xml' })
end
it "returns basic data + context when context='true'" do
get :search, params: { q: 'cornell', vocab: 'OCLC_FAST', maximumRecords: '3', context: 'true' }
expect(response).to be_successful
results = JSON.parse(response.body)
expect(results.size).to eq 3
expect(results.first.key?('context')).to be true
end

it "returns basic data only when context='false'" do
get :search, params: { q: 'cornell', vocab: 'OCLC_FAST', maximumRecords: '3', context: 'false' }
expect(response).to be_successful
results = JSON.parse(response.body)
expect(results.size).to eq 3
expect(results.first.key?('context')).to be false
end
end
end

describe '#show' do
Expand Down
123 changes: 123 additions & 0 deletions spec/services/linked_data/mapper/context_mapper_service_spec.rb
@@ -0,0 +1,123 @@
require 'spec_helper'

RSpec.describe Qa::LinkedData::Mapper::ContextMapperService do
subject { described_class.map_context(graph: graph, context_map: context_map, subject_uri: subject_uri) }

let(:graph) { instance_double(RDF::Graph) }
let(:context_map) { instance_double(Qa::LinkedData::Config::ContextMap) }
let(:subject_uri) { instance_double(RDF::URI) }

let(:context_properties) { [birth_date_property_map, death_date_property_map, occupation_property_map] }

let(:birth_date_property_map) { instance_double(Qa::LinkedData::Config::ContextPropertyMap) }
let(:death_date_property_map) { instance_double(Qa::LinkedData::Config::ContextPropertyMap) }
let(:occupation_property_map) { instance_double(Qa::LinkedData::Config::ContextPropertyMap) }

let(:group_id) { 'dates' }

let(:birth_date_values) { ['10/15/1943'] }
let(:death_date_values) { ['12/17/2018'] }
let(:occupation_values) { ['Actress', 'Director', 'Producer'] }

before do
allow(context_map).to receive(:properties).and_return(context_properties)
allow(context_map).to receive(:group_label).with('dates').and_return('Dates')

allow(birth_date_property_map).to receive(:label).and_return('Birth')
allow(birth_date_property_map).to receive(:values).with(graph, subject_uri).and_return(birth_date_values)
allow(birth_date_property_map).to receive(:group?).and_return(false)
allow(birth_date_property_map).to receive(:selectable?).and_return(false)
allow(birth_date_property_map).to receive(:drillable?).and_return(false)

allow(death_date_property_map).to receive(:label).and_return('Death')
allow(death_date_property_map).to receive(:values).with(graph, subject_uri).and_return(death_date_values)
allow(death_date_property_map).to receive(:group?).and_return(false)
allow(death_date_property_map).to receive(:selectable?).and_return(false)
allow(death_date_property_map).to receive(:drillable?).and_return(false)

allow(occupation_property_map).to receive(:label).and_return('Occupation')
allow(occupation_property_map).to receive(:values).with(graph, subject_uri).and_return(occupation_values)
allow(occupation_property_map).to receive(:group?).and_return(false)
allow(occupation_property_map).to receive(:selectable?).and_return(false)
allow(occupation_property_map).to receive(:drillable?).and_return(false)
end

describe '.map_context' do
it 'sets the property labels from the property map' do
find_property_to_test(subject, 'Birth')
find_property_to_test(subject, 'Death')
find_property_to_test(subject, 'Occupation')
expect(subject.size).to be 3
end

it 'sets the property values from the graph' do
result = find_property_to_test(subject, 'Birth')
expect(result['values']).to match_array birth_date_values
result = find_property_to_test(subject, 'Death')
expect(result['values']).to match_array death_date_values
result = find_property_to_test(subject, 'Occupation')
expect(result['values']).to match_array occupation_values
end

context 'when group? is false' do
before { allow(birth_date_property_map).to receive(:group?).and_return(false) }
it 'does not include group in results' do
result = find_property_to_test(subject, 'Birth')
expect(result.key?('group')).to be false
end
end

context 'when group? is true' do
before do
allow(birth_date_property_map).to receive(:group?).and_return(true)
allow(birth_date_property_map).to receive(:group_id).and_return('dates')
allow(context_map).to receive(:group_label).with('dates').and_return('Dates')
end

it 'includes group in results' do
result = find_property_to_test(subject, 'Birth')
expect(result['group']).to eq 'Dates'
end
end

context 'when drillable? is false' do
before { allow(death_date_property_map).to receive(:drillable?).and_return(false) }
it 'includes drillable set to false' do
result = find_property_to_test(subject, 'Death')
expect(result['drillable']).to be false
end
end

context 'when drillable? is true' do
before { allow(death_date_property_map).to receive(:drillable?).and_return(true) }
it 'includes drillable set to true' do
result = find_property_to_test(subject, 'Death')
expect(result['drillable']).to be true
end
end

context 'when selectable? is false' do
before { allow(occupation_property_map).to receive(:selectable?).and_return(false) }
it 'includes selectable set to false' do
result = find_property_to_test(subject, 'Occupation')
expect(result['selectable']).to be false
end
end

context 'when selectable? is true' do
before { allow(occupation_property_map).to receive(:selectable?).and_return(true) }
it 'includes selectable set to true' do
result = find_property_to_test(subject, 'Occupation')
expect(result['selectable']).to be true
end
end
end

def find_property_to_test(results, label)
results.each do |r|
next unless r['property'] == label
return r
end
raise "property (#{label}) to test not found"
end
end
Expand Up @@ -59,5 +59,51 @@
expect(subjects).not_to include "http://id.worldcat.org/fast/510103"
expect(subjects).not_to include "_:b0"
end

context 'when context_map is passed in' do
subject { described_class.map_values(graph: graph, predicate_map: predicate_map, sort_key: sort_key, context_map: context_map) }

let(:context_map) { instance_double(Qa::LinkedData::Config::ContextMap) }
let(:context) do
{ location: '42.4488° N, 76.4763° W' }
end
let(:expected530369_with_context) do
{
uri: [RDF::URI.new('http://id.worldcat.org/fast/530369')],
id: [RDF::Literal.new('530369')],
label: [RDF::Literal.new('Cornell University')],
altlabel: [RDF::Literal.new('Ithaca (N.Y.). Cornell University')],
sameas: [RDF::URI.new('http://id.loc.gov/authorities/names/n79021621')],
sort: [RDF::Literal.new('1')],
context: context
}
end
let(:expected5140_with_context) do
{
uri: [RDF::URI.new('http://id.worldcat.org/fast/5140')],
id: [RDF::Literal.new('5140')],
label: [RDF::Literal.new('Cornell, Joseph')],
altlabel: [RDF::URI.new('_:b0')],
sameas: [],
sort: [RDF::Literal.new('3')],
context: context
}
end

before do
allow(Qa::LinkedData::Mapper::ContextMapperService).to receive(:map_context).with(graph: anything, context_map: anything, subject_uri: anything).and_return(context)
end

it 'adds context if requested' do
expect(subject.count).to eq 2
expect(subject).to be_kind_of Array
expect(subjects).to eq ["http://id.worldcat.org/fast/530369", "http://id.worldcat.org/fast/5140"]

actual530369 = subject.first
actual5140 = subject.second
expect(actual530369).to eq expected530369_with_context
expect(actual5140).to eq expected5140_with_context
end
end
end
end

0 comments on commit c956715

Please sign in to comment.