qa 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/app/controllers/qa/linked_data_terms_controller.rb +30 -9
  4. data/app/controllers/qa/terms_controller.rb +3 -2
  5. data/app/models/qa/linked_data/config/context_property_map.rb +6 -25
  6. data/app/services/qa/iri_template_service.rb +32 -24
  7. data/app/services/qa/linked_data/authority_service.rb +8 -0
  8. data/app/services/qa/linked_data/authority_url_service.rb +27 -8
  9. data/app/services/qa/linked_data/deep_sort_service.rb +3 -2
  10. data/app/services/qa/linked_data/graph_service.rb +13 -0
  11. data/app/services/qa/linked_data/language_service.rb +12 -0
  12. data/app/services/qa/linked_data/language_sort_service.rb +7 -2
  13. data/app/services/qa/linked_data/ldpath_service.rb +40 -0
  14. data/app/services/qa/linked_data/mapper/graph_ldpath_mapper_service.rb +49 -0
  15. data/app/services/qa/linked_data/mapper/graph_mapper_service.rb +3 -11
  16. data/app/services/qa/linked_data/mapper/graph_predicate_mapper_service.rb +40 -0
  17. data/app/services/qa/linked_data/mapper/search_results_mapper_service.rb +58 -11
  18. data/app/services/qa/linked_data/mapper/term_results_mapper_service.rb +80 -0
  19. data/config/authorities/linked_data/loc.json +13 -7
  20. data/config/authorities/linked_data/oclc_fast.json +13 -8
  21. data/lib/generators/qa/discogs/USAGE +10 -0
  22. data/lib/generators/qa/discogs/discogs_generator.rb +12 -0
  23. data/lib/generators/qa/discogs/templates/config/discogs-formats.yml +346 -0
  24. data/lib/generators/qa/discogs/templates/config/discogs-genres.yml +627 -0
  25. data/lib/generators/qa/install/templates/config/initializers/qa.rb +4 -0
  26. data/lib/qa.rb +6 -0
  27. data/lib/qa/authorities.rb +2 -0
  28. data/lib/qa/authorities/discogs.rb +28 -0
  29. data/lib/qa/authorities/discogs/discogs_instance_builder.rb +145 -0
  30. data/lib/qa/authorities/discogs/discogs_translation.rb +126 -0
  31. data/lib/qa/authorities/discogs/discogs_utils.rb +89 -0
  32. data/lib/qa/authorities/discogs/discogs_works_builder.rb +153 -0
  33. data/lib/qa/authorities/discogs/generic_authority.rb +151 -0
  34. data/lib/qa/authorities/discogs_subauthority.rb +9 -0
  35. data/lib/qa/authorities/linked_data/config.rb +7 -3
  36. data/lib/qa/authorities/linked_data/config/search_config.rb +99 -11
  37. data/lib/qa/authorities/linked_data/config/term_config.rb +112 -8
  38. data/lib/qa/authorities/linked_data/find_term.rb +154 -84
  39. data/lib/qa/authorities/linked_data/search_query.rb +76 -13
  40. data/lib/qa/configuration.rb +8 -0
  41. data/lib/qa/version.rb +1 -1
  42. data/spec/controllers/linked_data_terms_controller_spec.rb +151 -30
  43. data/spec/controllers/terms_controller_spec.rb +4 -0
  44. data/spec/features/linked_data/language_spec.rb +298 -0
  45. data/spec/fixtures/authorities/linked_data/lod_full_config.json +21 -5
  46. data/spec/fixtures/authorities/linked_data/lod_lang_defaults.json +4 -4
  47. data/spec/fixtures/authorities/linked_data/lod_lang_multi_defaults.json +4 -4
  48. data/spec/fixtures/authorities/linked_data/lod_lang_no_defaults.json +4 -5
  49. data/spec/fixtures/authorities/linked_data/lod_lang_param.json +4 -4
  50. data/spec/fixtures/authorities/linked_data/lod_term_uri_param_config.json +1 -1
  51. data/spec/fixtures/discogs-find-response-json.json +1 -0
  52. data/spec/fixtures/discogs-find-response-jsonld-master.json +1 -0
  53. data/spec/fixtures/discogs-find-response-jsonld-release.json +1 -0
  54. data/spec/fixtures/discogs-id-matches-master.json +1 -0
  55. data/spec/fixtures/discogs-id-matches-release.json +1 -0
  56. data/spec/fixtures/discogs-id-not-found-master.json +1 -0
  57. data/spec/fixtures/discogs-id-not-found-release.json +1 -0
  58. data/spec/fixtures/discogs-search-response-no-auth.json +1 -0
  59. data/spec/fixtures/discogs-search-response-no-subauth.json +1 -0
  60. data/spec/fixtures/discogs-search-response-subauth.json +1 -0
  61. data/spec/fixtures/lod_lang_search_enesfrde.rdf.xml +60 -0
  62. data/spec/fixtures/lod_lang_search_sv.rdf.xml +42 -0
  63. data/spec/fixtures/lod_loc_term_found.rdf.xml +5 -0
  64. data/spec/lib/authorities/discogs/generic_authority_spec.rb +235 -0
  65. data/spec/lib/authorities/discogs_spec.rb +17 -0
  66. data/spec/lib/authorities/linked_data/config_spec.rb +68 -5
  67. data/spec/lib/authorities/linked_data/find_term_spec.rb +298 -3
  68. data/spec/lib/authorities/linked_data/generic_authority_spec.rb +46 -485
  69. data/spec/lib/authorities/linked_data/search_config_spec.rb +154 -3
  70. data/spec/lib/authorities/linked_data/search_query_spec.rb +240 -3
  71. data/spec/lib/authorities/linked_data/term_config_spec.rb +193 -5
  72. data/spec/lib/configuration_spec.rb +18 -0
  73. data/spec/models/linked_data/config/context_property_map_spec.rb +3 -31
  74. data/spec/services/iri_template_service_spec.rb +54 -12
  75. data/spec/{lib/authorities → services}/linked_data/authority_service_spec.rb +47 -0
  76. data/spec/services/linked_data/language_service_spec.rb +52 -11
  77. data/spec/services/linked_data/ldpath_service_spec.rb +61 -0
  78. data/spec/services/linked_data/mapper/graph_ldpath_mapper_service_spec.rb +118 -0
  79. data/spec/services/linked_data/mapper/graph_predicate_mapper_service_spec.rb +110 -0
  80. data/spec/services/linked_data/mapper/term_results_mapper_service_spec.rb +94 -0
  81. data/spec/spec_helper.rb +1 -1
  82. data/spec/support/matchers/include_hash.rb +5 -0
  83. data/spec/test_app_templates/lib/generators/test_app_generator.rb +4 -0
  84. metadata +73 -5
  85. data/lib/qa/authorities/linked_data/rdf_helper.rb +0 -49
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ab8962fbfa1793451a7cc2d54cb7dfac7b78888c
4
- data.tar.gz: 05aa5d8b96b64e9de68996ab84a00e3e79ab4d8c
3
+ metadata.gz: f8ae75788063293cff50f32548b9eccbd6d0c837
4
+ data.tar.gz: 3aad18f6f86d31d4e8a13941407c8890d1389a21
5
5
  SHA512:
6
- metadata.gz: 0c3c034f33d2cfb2c1f1062eac7423d05c2dcca1bc23106a2c14d7f62e39b83c7c8592c07685673cda14e0c58ab734666d4185a8be55ab2266218421ce051e29
7
- data.tar.gz: 3e139a9edf37c8b5fe9a12a37a8e2e79a507943ef41d502c3b6c56afbfa4eadbb9fe2aa2dbe6ba3079a47b2b4c365f942078fa86b13561d877fb6acdb8b524c2
6
+ metadata.gz: 4c1bd3ffc5d4d6d663fa656cebc9400f390a393862c3d68c454866f42f50f803236f64cd97dc3c0a63e5e895e2ca575b2f32b5d9e5372c6fe572bf08b1484397
7
+ data.tar.gz: 761ef1f50ecdfdc425a8deb49b3672508955725aa3339514c517b8bbc0cd6d1285b689adde5d35e2e3aa877e659ead79492c6ad6d82de345bd6c3cb230245f93
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Code:
4
4
  [![Gem Version](https://badge.fury.io/rb/qa.png)](http://badge.fury.io/rb/qa)
5
- [![Build Status](https://travis-ci.org/samvera/questioning_authority.png?branch=master)](https://travis-ci.org/samvera/questioning_authority)
5
+ [![Build Status](https://circleci.com/gh/samvera/questioning_authority.svg?style=svg)](https://circleci.com/gh/samvera/questioning_authority)
6
6
  [![Coverage Status](https://coveralls.io/repos/github/samvera/questioning_authority/badge.svg?branch=master)](https://coveralls.io/github/samvera/questioning_authority?branch=master)
7
7
 
8
8
  Docs:
@@ -16,11 +16,12 @@ class Qa::LinkedDataTermsController < ::ApplicationController
16
16
  head :not_found
17
17
  end
18
18
 
19
- # Return a list of supported authority names
20
- # get "/list/linked_data/authorities"
19
+ # Return a list of supported authority names optionally with details about the authority
20
+ # get "/list/linked_data/authorities?details=true|false" (default details=false)
21
21
  # @see Qa::LinkedData::AuthorityService#authority_names
22
+ # @see Qa::LinkedData::AuthorityService#authority_details
22
23
  def list
23
- render json: Qa::LinkedData::AuthorityService.authority_names.to_json
24
+ details? ? render_detail_list : render_simple_list
24
25
  end
25
26
 
26
27
  # Reload authority configurations
@@ -34,8 +35,8 @@ class Qa::LinkedDataTermsController < ::ApplicationController
34
35
  # Return a list of terms based on a query
35
36
  # get "/search/linked_data/:vocab(/:subauthority)"
36
37
  # @see Qa::Authorities::LinkedData::SearchQuery#search
37
- def search # rubocop:disable Metrics/MethodLength
38
- terms = @authority.search(query, subauth: subauthority, language: language, replacements: replacement_params, context: context?)
38
+ def search # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
39
+ terms = @authority.search(query, subauth: subauthority, language: language, replacements: replacement_params, context: context?, performance_data: performance_data?)
39
40
  cors_allow_origin_header(response)
40
41
  render json: terms
41
42
  rescue Qa::ServiceUnavailable
@@ -58,7 +59,7 @@ class Qa::LinkedDataTermsController < ::ApplicationController
58
59
  # get "/show/linked_data/:vocab/:subauthority/:id
59
60
  # @see Qa::Authorities::LinkedData::FindTerm#find
60
61
  def show # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
61
- term = @authority.find(id, subauth: subauthority, language: language, replacements: replacement_params, jsonld: jsonld?)
62
+ term = @authority.find(id, subauth: subauthority, language: language, replacements: replacement_params, jsonld: jsonld?, performance_data: performance_data?)
62
63
  cors_allow_origin_header(response)
63
64
  content_type = jsonld? ? 'application/ld+json' : 'application/json'
64
65
  render json: term, content_type: content_type
@@ -85,7 +86,7 @@ class Qa::LinkedDataTermsController < ::ApplicationController
85
86
  # get "/fetch/linked_data/:vocab"
86
87
  # @see Qa::Authorities::LinkedData::FindTerm#find
87
88
  def fetch # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
88
- term = @authority.find(uri, subauth: subauthority, language: language, replacements: replacement_params, jsonld: jsonld?)
89
+ term = @authority.find(uri, subauth: subauthority, language: language, replacements: replacement_params, jsonld: jsonld?, performance_data: performance_data?)
89
90
  cors_allow_origin_header(response)
90
91
  content_type = jsonld? ? 'application/ld+json' : 'application/json'
91
92
  render json: term, content_type: content_type
@@ -110,6 +111,14 @@ class Qa::LinkedDataTermsController < ::ApplicationController
110
111
 
111
112
  private
112
113
 
114
+ def render_simple_list
115
+ render json: Qa::LinkedData::AuthorityService.authority_names.to_json
116
+ end
117
+
118
+ def render_detail_list
119
+ render json: Qa::LinkedData::AuthorityService.authority_details.to_json
120
+ end
121
+
113
122
  def check_authority
114
123
  if params[:vocab].nil? || !params[:vocab].size.positive? # rubocop:disable Style/GuardClause
115
124
  msg = "Required param 'vocab' is missing or empty"
@@ -180,7 +189,9 @@ class Qa::LinkedDataTermsController < ::ApplicationController
180
189
  end
181
190
 
182
191
  def language
183
- params[:lang]
192
+ request_language = request.env['HTTP_ACCEPT_LANGUAGE']
193
+ request_language = request_language.scan(/^[a-z]{2}/).first if request_language.present?
194
+ params[:lang] || request_language
184
195
  end
185
196
 
186
197
  def subauthority
@@ -188,7 +199,7 @@ class Qa::LinkedDataTermsController < ::ApplicationController
188
199
  end
189
200
 
190
201
  def replacement_params
191
- params.reject { |k, _v| ['q', 'vocab', 'controller', 'action', 'subauthority', 'language', 'id'].include?(k) }
202
+ params.reject { |k, _v| ['q', 'vocab', 'controller', 'action', 'subauthority', 'lang', 'id'].include?(k) }
192
203
  end
193
204
 
194
205
  def subauth_warn_msg
@@ -210,6 +221,16 @@ class Qa::LinkedDataTermsController < ::ApplicationController
210
221
  context.casecmp('true').zero?
211
222
  end
212
223
 
224
+ def details?
225
+ details = params.fetch(:details, 'false')
226
+ details.casecmp('true').zero?
227
+ end
228
+
229
+ def performance_data?
230
+ performance_data = params.fetch(:performance_data, 'false')
231
+ performance_data.casecmp('true').zero?
232
+ end
233
+
213
234
  def validate_auth_reload_token
214
235
  token = params.key?(:auth_token) ? params[:auth_token] : nil
215
236
  valid = Qa.config.valid_authority_reload_token?(token)
@@ -28,9 +28,10 @@ class Qa::TermsController < ::ApplicationController
28
28
 
29
29
  # If the subauthority supports it, return all the information for a given term
30
30
  def show
31
- term = @authority.find(params[:id])
31
+ term = @authority.method(:find).arity == 2 ? @authority.find(params[:id], self) : @authority.find(params[:id])
32
32
  cors_allow_origin_header(response)
33
- render json: term
33
+ content_type = params["format"] == "jsonld" ? 'application/ld+json' : 'application/json'
34
+ render json: term, content_type: content_type
34
35
  end
35
36
 
36
37
  def check_vocab_param
@@ -67,7 +67,7 @@ module Qa
67
67
  # Values of this property for a specfic subject URI
68
68
  # @return [Array<String>] values for this property
69
69
  def values(graph, subject_uri)
70
- ldpath_evaluate(basic_program, graph, subject_uri)
70
+ Qa::LinkedData::LdpathService.ldpath_evaluate(program: basic_program, graph: graph, subject_uri: subject_uri)
71
71
  end
72
72
 
73
73
  # Values of this property for a specfic subject URI with URI values expanded to include id and label.
@@ -98,44 +98,25 @@ module Qa
98
98
  end
99
99
 
100
100
  def basic_program
101
- @basic_program ||= ldpath_program(ldpath)
101
+ @basic_program ||= Qa::LinkedData::LdpathService.ldpath_program(ldpath: ldpath, prefixes: prefixes)
102
102
  end
103
103
 
104
104
  def expansion_label_program
105
- @expansion_label_program ||= ldpath_program(expansion_label_ldpath)
105
+ @expansion_label_program ||= Qa::LinkedData::LdpathService.ldpath_program(ldpath: expansion_label_ldpath, prefixes: prefixes)
106
106
  end
107
107
 
108
108
  def expansion_id_program
109
- @expansion_id_program ||= ldpath_program(expansion_id_ldpath)
110
- end
111
-
112
- def ldpath_program(ldpath)
113
- program_code = ""
114
- prefixes.each { |key, url| program_code << "@prefix #{key} : <#{url}> \;\n" }
115
- program_code << "property = #{ldpath} \;"
116
- Ldpath::Program.parse program_code
117
- rescue => e
118
- Rails.logger.warn("WARNING: #{I18n.t('qa.linked_data.ldpath.parse_logger_error')} (ldpath='#{ldpath}')\n cause: #{e.message}")
119
- raise StandardError, I18n.t('qa.linked_data.ldpath.parse_error')
120
- end
121
-
122
- def ldpath_evaluate(program, graph, subject_uri)
123
- return VALUE_ON_ERROR if program.blank?
124
- output = program.evaluate subject_uri, graph
125
- output.present? ? output['property'].uniq : nil
126
- rescue => e
127
- Rails.logger.warn("WARNING: #{I18n.t('qa.linked_data.ldpath.evaluate_logger_error')} (ldpath='#{ldpath}')\n cause: #{e.message}")
128
- raise StandardError, I18n.t('qa.linked_data.ldpath.evaluate_error')
109
+ @expansion_id_program ||= Qa::LinkedData::LdpathService.ldpath_program(ldpath: expansion_id_ldpath, prefixes: prefixes)
129
110
  end
130
111
 
131
112
  def expansion_label(graph, uri)
132
- label = ldpath_evaluate(expansion_label_program, graph, RDF::URI(uri))
113
+ label = Qa::LinkedData::LdpathService.ldpath_evaluate(program: expansion_label_program, graph: graph, subject_uri: RDF::URI(uri))
133
114
  label.size == 1 ? label.first : label
134
115
  end
135
116
 
136
117
  def expansion_id(graph, uri)
137
118
  return uri if expansion_id_ldpath.blank?
138
- id = ldpath_evaluate(expansion_id_program, graph, RDF::URI(uri))
119
+ id = Qa::LinkedData::LdpathService.ldpath_evaluate(program: expansion_id_program, graph: graph, subject_uri: RDF::URI(uri))
139
120
  id.size == 1 ? id.first : id
140
121
  end
141
122
  end
@@ -1,31 +1,39 @@
1
1
  # Provide service for building a URL based on an IRI Templated Link and its variable mappings based on provided substitutions.
2
2
  module Qa
3
3
  class IriTemplateService
4
- # Construct an url from an IriTemplate making identified substitutions
5
- # @param url_config [Qa::IriTemplate::UrlConfig] configuration (json) holding the template and variable mappings
6
- # @param substitutions [HashWithIndifferentAccess] name-value pairs to substitute into the url template
7
- # @return [String] url with substitutions
8
- def self.build_url(url_config:, substitutions:)
9
- # TODO: This is a very simple approach using direct substitution into the template string.
10
- # Better would be to...
11
- # * patterns without a substitution are not included in the resulting URL
12
- # * appropriately adds '?' or '&'
13
- # * ensure proper escaping of values (e.g. value="A simple string" which is encoded as A%20simple%20string)
14
- # Even more advanced would be to...
15
- # * support BasicRepresentation (which is what it does now)
16
- # * support ExplicitRepresentation
17
- # * literal encoding for values (e.g. value="A simple string" becomes %22A%20simple%20string%22)
18
- # * 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)
19
- # * type encoding for values (e.g. value=5.5 becomes value="5.5"^^http://www.w3.org/2001/XMLSchema#decimal which is encoded
20
- # as %225.5%22%5E%5Ehttp%3A%2F%2Fwww.w3.org%2F2001%2FXMLSchema%23decimal)
21
- # Fuller implementations parse the template into component parts and then build the URL by adding parts in as applicable.
22
- url = url_config.template
23
- url_config.mapping.each do |m|
24
- key = m.variable
25
- url = url.gsub("{#{key}}", m.simple_value(substitutions[key]))
26
- url = url.gsub("{?#{key}}", m.parameter_value(substitutions[key]))
4
+ class << self
5
+ # Construct an url from an IriTemplate making identified substitutions
6
+ # @param url_config [Qa::IriTemplate::UrlConfig] configuration (json) holding the template and variable mappings
7
+ # @param substitutions [HashWithIndifferentAccess] name-value pairs to substitute into the url template
8
+ # @return [String] url with substitutions
9
+ def build_url(url_config:, substitutions:)
10
+ # TODO: This is a very simple approach using direct substitution into the template string.
11
+ # Better would be to...
12
+ # * appropriately adds '?' or '&'
13
+ # * ensure proper escaping of values (e.g. value="A simple string" which is encoded as A%20simple%20string)
14
+ # Even more advanced would be to...
15
+ # * support BasicRepresentation (which is what it does now)
16
+ # * support ExplicitRepresentation
17
+ # * literal encoding for values (e.g. value="A simple string" becomes %22A%20simple%20string%22)
18
+ # * 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)
19
+ # * type encoding for values (e.g. value=5.5 becomes value="5.5"^^http://www.w3.org/2001/XMLSchema#decimal which is encoded
20
+ # as %225.5%22%5E%5Ehttp%3A%2F%2Fwww.w3.org%2F2001%2FXMLSchema%23decimal)
21
+ # Fuller implementations parse the template into component parts and then build the URL by adding parts in as applicable.
22
+ url = url_config.template
23
+ url_config.mapping.each do |m|
24
+ key = m.variable
25
+ url = url.gsub("{#{key}}", m.simple_value(substitutions[key]))
26
+ url = url.gsub("{?#{key}}", m.parameter_value(substitutions[key]))
27
+ end
28
+ clean(url)
27
29
  end
28
- url
30
+
31
+ private
32
+
33
+ # In the process of substitution, if a value is missing, you can end up with '?&', '&&', or ending with '&'. These are all removed this method.
34
+ def clean(url)
35
+ url.gsub(/&&*/, '&').chomp('&').gsub('?&', '?')
36
+ end
29
37
  end
30
38
  end
31
39
  end
@@ -41,6 +41,14 @@ module Qa
41
41
  def self.authority_names
42
42
  authority_configs.keys.sort
43
43
  end
44
+
45
+ # Get the list of names and details of the loaded authorities
46
+ # @return [Array<String>] names of the authority config files that are currently loaded
47
+ def self.authority_details
48
+ details = []
49
+ authority_names.each { |auth_name| details << Qa::Authorities::LinkedData::Config.new(auth_name).authority_info }
50
+ details.flatten
51
+ end
44
52
  end
45
53
  end
46
54
  end
@@ -4,16 +4,17 @@ module Qa
4
4
  class AuthorityUrlService
5
5
  class << self
6
6
  # Build a url for an authority/subauthority for the specified action.
7
- # @param authority [Symbol] name of a registered authority
8
- # @param subauthority [String] name of a subauthority
7
+ # @param action_config [Qa::Authorities::LinkedData::SearchConfig | Qa::Authorities::LinkedData::TermConfig] action configuration for the authority
9
8
  # @param action [Symbol] action with valid values :search or :term
10
9
  # @param action_request [String] the request the user is making of the authority (e.g. query text or term id/uri)
11
- # @param substitutions [Hash] variable-value pairs to substitute into the URL template
12
- # @return a valid URL the submits the action request to the external authority
13
- def build_url(action_config:, action:, action_request:, substitutions: {}, subauthority: nil)
10
+ # @param substitutions [Hash] variable-value pairs to substitute into the URL template (optional)
11
+ # @param subauthority [String] name of a subauthority (optional)
12
+ # @param language [Array<Symbol>] languages for filtering returned literals (optional)
13
+ # @return a valid URL that submits the action request to the external authority
14
+ def build_url(action_config:, action:, action_request:, substitutions: {}, subauthority: nil, language: nil) # rubocop:disable Metrics/ParameterLists
14
15
  action_validation(action)
15
16
  url_config = action_config.url_config
16
- selected_substitutions = url_config.extract_substitutions(combined_substitutions(action_config, action, action_request, substitutions, subauthority))
17
+ selected_substitutions = url_config.extract_substitutions(combined_substitutions(action_config, action, action_request, substitutions, subauthority, language))
17
18
  Qa::IriTemplateService.build_url(url_config: url_config, substitutions: selected_substitutions)
18
19
  end
19
20
 
@@ -24,9 +25,10 @@ module Qa
24
25
  raise Qa::UnsupportedAction, "#{action} Not Supported - Action must be one of the supported actions (e.g. :term, :search)"
25
26
  end
26
27
 
27
- def combined_substitutions(action_config, action, action_request, substitutions, subauthority)
28
+ def combined_substitutions(action_config, action, action_request, substitutions, subauthority, language) # rubocop:disable Metrics/ParameterLists
28
29
  substitutions[action_request_variable(action_config, action)] = action_request
29
- substitutions[action_subauth_variable(action_config)] = action_subauth_variable_value(action_config, subauthority) if subauthority.present?
30
+ substitutions[action_subauth_variable(action_config)] = action_subauth_variable_value(action_config, subauthority) if supports_subauthorities?(action_config) && subauthority.present?
31
+ substitutions[action_language_variable(action_config)] = language_value(language) if supports_language_parameter?(action_config) && language.present?
30
32
  substitutions
31
33
  end
32
34
 
@@ -35,6 +37,10 @@ module Qa
35
37
  action_config.qa_replacement_patterns[key]
36
38
  end
37
39
 
40
+ def supports_subauthorities?(action_config)
41
+ action_config.supports_subauthorities?
42
+ end
43
+
38
44
  def action_subauth_variable(action_config)
39
45
  action_config.qa_replacement_patterns[:subauth]
40
46
  end
@@ -42,6 +48,19 @@ module Qa
42
48
  def action_subauth_variable_value(action_config, subauthority)
43
49
  action_config.subauthorities[subauthority.to_sym]
44
50
  end
51
+
52
+ def supports_language_parameter?(action_config)
53
+ action_config.supports_language_parameter?
54
+ end
55
+
56
+ def action_language_variable(action_config)
57
+ action_config.qa_replacement_patterns[:lang]
58
+ end
59
+
60
+ def language_value(language)
61
+ return nil if language.blank?
62
+ language.first
63
+ end
45
64
  end
46
65
  end
47
66
  end
@@ -2,8 +2,9 @@
2
2
  module Qa
3
3
  module LinkedData
4
4
  class DeepSortService
5
- # @params [Array<Hash<Symbol,Array<RDF::Literal>>>] the array of hashes to sort
6
- # @params [sort_key] the key in the hash on whose value the array will be sorted
5
+ # @param [Array<Hash<Symbol,Array<RDF::Literal>>>] the array of hashes to sort
6
+ # @param [sort_key] the key in the hash on whose value the array will be sorted
7
+ # @param [Symbol] preferred language to appear first in the list; defaults to no preference
7
8
  # @return instance of this class
8
9
  # @example the_array parameter
9
10
  # [
@@ -41,6 +41,19 @@ module Qa
41
41
  values
42
42
  end
43
43
 
44
+ # Get subjects that have the object value for the predicate in the graph.
45
+ # @param graph [RDF::Graph] the graph to search
46
+ # @param predicate [RDF::URI] the URI of the predicate
47
+ # @param object_value [String] the object value
48
+ # @return [Array] all subjects for the predicate-object_value pair
49
+ def subjects_for_object_value(graph:, predicate:, object_value:)
50
+ subjects = []
51
+ graph.query([:subject, predicate, object_value]) do |statement|
52
+ subjects << statement.subject
53
+ end
54
+ subjects
55
+ end
56
+
44
57
  def deep_copy(graph:)
45
58
  new_graph = RDF::Graph.new
46
59
  graph.statements.each do |st|
@@ -2,13 +2,24 @@
2
2
  module Qa
3
3
  module LinkedData
4
4
  class LanguageService
5
+ WILDCARD = '*'.freeze
6
+
5
7
  class << self
8
+ # @param user_langauge [Symbol|String] the language code (e.g. :en, :fr) specified as URL parameter or on URL header
9
+ # @param authority_language [Symbol|String|Array<Symbol|String>] the default language specified in the authority's configuration file
10
+ # @return [Array<Symbol>] the selected language(s) normalized as an array of symbols (e.g. [:en, :fr])
11
+ # @note Precedence from high to low:
12
+ # * user_language (with URL parameter preferred over URL header)
13
+ # * authority_language (defined in authority config)
14
+ # * site default_language (defined in QA initializer)
6
15
  def preferred_language(user_language: nil, authority_language: nil)
7
16
  return normalize_language(user_language) if user_language.present?
8
17
  return normalize_language(authority_language) if authority_language.present?
9
18
  normalize_language(Qa.config.default_language)
10
19
  end
11
20
 
21
+ # @param [RDF::Literal] the literal to check
22
+ # @return [Boolean] true if literal has a language tag; otherwise, false
12
23
  def literal_has_language_marker?(literal)
13
24
  return false unless literal.respond_to?(:language)
14
25
  literal.language.present?
@@ -22,6 +33,7 @@ module Qa
22
33
  def normalize_language(language)
23
34
  return language if language.blank?
24
35
  language = [language] unless language.is_a? Array
36
+ return nil if language.include?(WILDCARD)
25
37
  language.map(&:to_sym)
26
38
  end
27
39
  end
@@ -7,7 +7,6 @@ module Qa
7
7
  attr_reader :literals, :preferred_language
8
8
  attr_reader :languages, :bins
9
9
  private :literals, :preferred_language, :languages, :bins
10
- # private :literals, :preferred_language, :languages, :languages=, :bins, :bins=
11
10
 
12
11
  # @param [Array<RDF::Literals>] string literals to sort
13
12
  # @param [Symbol] preferred language to appear first in the list; defaults to no preference
@@ -20,7 +19,7 @@ module Qa
20
19
  end
21
20
 
22
21
  # Sort the literals stored in this instance of the service
23
- # @return sorted version of literals
22
+ # @return [Array<RDF::Literals] sorted version of literals
24
23
  def sort
25
24
  return literals unless literals.present?
26
25
  return @sorted_literals if @sorted_literals.present?
@@ -30,6 +29,12 @@ module Qa
30
29
  @sorted_literals = construct_sorted_literals
31
30
  end
32
31
 
32
+ # Sort the literals and return as an array of strings with only unique literals and empty strings removed
33
+ # @return [Array<String>] sorted version of literals as strings
34
+ def uniq_sorted_strings
35
+ sort.map(&:to_s).uniq.delete_if(&:blank?)
36
+ end
37
+
33
38
  private
34
39
 
35
40
  def construct_sorted_literals
@@ -0,0 +1,40 @@
1
+ # Defines the external authority predicates used to extract additional context from the graph.
2
+ require 'ldpath'
3
+
4
+ module Qa
5
+ module LinkedData
6
+ class LdpathService
7
+ VALUE_ON_ERROR = [].freeze
8
+
9
+ # Create the ldpath program for a given ldpath.
10
+ # @param ldpath [String] ldpath to follow to get a value from a graph (documation: http://marmotta.apache.org/ldpath/language.html)
11
+ # @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#" })
12
+ # @return [Ldpath::Program] an executable program that will extract a value from a graph
13
+ def self.ldpath_program(ldpath:, prefixes: {})
14
+ program_code = ""
15
+ prefixes.each { |key, url| program_code << "@prefix #{key} : <#{url}> \;\n" }
16
+ program_code << "property = #{ldpath} \;"
17
+ Ldpath::Program.parse program_code
18
+ rescue => e
19
+ Rails.logger.warn("WARNING: #{I18n.t('qa.linked_data.ldpath.parse_logger_error')}... cause: #{e.message}\n ldpath_program=\n#{program_code}")
20
+ raise StandardError, I18n.t("qa.linked_data.ldpath.parse_error") + "... cause: #{e.message}"
21
+ end
22
+
23
+ # Evaluate an ldpath for a specific subject uri in the context of a graph and return the extracted values.
24
+ # @param program [Ldpath::Program] an executable program that will extract a value from a graph
25
+ # @param graph [RDF::Graph] the graph from which the values will be extracted
26
+ # @param subject_uri [RDF::URI] retrieved values will be limited to those with the subject uri
27
+ # @param limit_to_context [Boolean] if true, the evaluation process will not make any outside network calls.
28
+ # It will limit results to those found in the context graph.
29
+ ## @return [Array<String>] the extracted values based on the ldpath
30
+ def self.ldpath_evaluate(program:, graph:, subject_uri:, limit_to_context: Qa.config.limit_ldpath_to_context?)
31
+ return VALUE_ON_ERROR if program.blank?
32
+ output = program.evaluate(subject_uri, context: graph, limit_to_context: limit_to_context)
33
+ output.present? ? output['property'].uniq : nil
34
+ rescue => e
35
+ Rails.logger.warn("WARNING: #{I18n.t('qa.linked_data.ldpath.evaluate_logger_error')} (cause: #{e.message}")
36
+ raise StandardError, I18n.t("qa.linked_data.ldpath.evaluate_error") + "... cause: #{e.message}"
37
+ end
38
+ end
39
+ end
40
+ end