qa 4.0.0 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
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