qa-ldf 0.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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rubocop.yml +21 -0
  4. data/.travis.yml +15 -0
  5. data/.yardopts +9 -0
  6. data/CONTRIBUTING.md +109 -0
  7. data/Gemfile +18 -0
  8. data/Guardfile +27 -0
  9. data/LICENSE +14 -0
  10. data/README.md +42 -0
  11. data/Rakefile +30 -0
  12. data/VERSION +1 -0
  13. data/config/ldf.yml +15 -0
  14. data/lib/qa/ldf.rb +43 -0
  15. data/lib/qa/ldf/authorities.rb +1 -0
  16. data/lib/qa/ldf/authorities/lc_names.rb +31 -0
  17. data/lib/qa/ldf/authority.rb +104 -0
  18. data/lib/qa/ldf/client.rb +39 -0
  19. data/lib/qa/ldf/configuration.rb +67 -0
  20. data/lib/qa/ldf/empty_search_service.rb +18 -0
  21. data/lib/qa/ldf/json_mapper.rb +35 -0
  22. data/lib/qa/ldf/model.rb +49 -0
  23. data/lib/qa/ldf/spec.rb +3 -0
  24. data/lib/qa/ldf/spec/authority.rb +112 -0
  25. data/lib/qa/ldf/spec/model.rb +9 -0
  26. data/lib/qa/ldf/spec/search_service.rb +30 -0
  27. data/lib/qa/ldf/version.rb +34 -0
  28. data/qa-ldf.gemspec +35 -0
  29. data/spec/contracts/qa_loc_as_search_service_spec.rb +38 -0
  30. data/spec/qa/ldf/authorities/lc_names_spec.rb +52 -0
  31. data/spec/qa/ldf/authority_spec.rb +29 -0
  32. data/spec/qa/ldf/client_spec.rb +23 -0
  33. data/spec/qa/ldf/configuration_spec.rb +93 -0
  34. data/spec/qa/ldf/empty_search_service_spec.rb +11 -0
  35. data/spec/qa/ldf/json_mapper_spec.rb +22 -0
  36. data/spec/qa/ldf/model_spec.rb +53 -0
  37. data/spec/qa/ldf_spec.rb +43 -0
  38. data/spec/spec_helper.rb +18 -0
  39. data/spec/support/cache_server.rb +25 -0
  40. data/spec/support/fake_client.rb +31 -0
  41. data/spec/support/fake_search_service.rb +40 -0
  42. data/spec/support/shared_examples/ld_cache_client.rb +35 -0
  43. metadata +232 -0
@@ -0,0 +1,39 @@
1
+ module Qa
2
+ module LDF
3
+ ##
4
+ # A client for the LD cache server
5
+ class Client
6
+ ##
7
+ # @yield [Client] yields self to a block if given
8
+ def initialize
9
+ yield self if block_given?
10
+ end
11
+
12
+ ##
13
+ # @param uri [String] a URI-like string
14
+ # @param dataset [Symbol]
15
+ #
16
+ # @return [RDF::Graph]
17
+ #
18
+ # @see RDF::Mutable#load
19
+ # @see RDF::LinkedDataFragments::CacheServer
20
+ def get(uri:, dataset: :'')
21
+ RDF::Graph.load(cache_uri(uri, dataset))
22
+ end
23
+
24
+ private
25
+
26
+ ##
27
+ # @param uri [String] a URI-like string
28
+ # @param dataset [Symbol]
29
+ #
30
+ # @return [RDF::URI]
31
+ def cache_uri(uri, dataset)
32
+ cache_uri = RDF::URI(Qa::LDF::Configuration.instance[:endpoint])
33
+ cache_uri.query = "subject=#{uri}"
34
+ cache_uri = cache_uri / 'dataset' / dataset unless dataset.empty?
35
+ cache_uri
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+ require 'singleton'
3
+
4
+ module Qa
5
+ module LDF
6
+ ##
7
+ # A singleton configuration class.
8
+ #
9
+ # This behaves much like a hash, but guarantees that only one configuration
10
+ # exists and accepts a block sytax for configuration.
11
+ #
12
+ # @example configuring with a configuration hash
13
+ # Qa::LDF::Configuration.instance
14
+ # .configure!(endpoint: 'http://example.org/ldf/')
15
+ #
16
+ # @example configuring with block syntax
17
+ # Qa::LDF::Configuration.instance.configure! do |config|
18
+ # config[:endpoint] = 'http://example.org/ldf/'
19
+ # end
20
+ #
21
+ # @see Qa::LDF.config
22
+ # @see Qa::LDF.configure!
23
+ class Configuration
24
+ extend Forwardable
25
+ include Singleton
26
+
27
+ ##
28
+ # @!macro [attach] def_delegator
29
+ # @!method $3()
30
+ # Delegates to the underlying options
31
+ def_delegator :@options, :'[]', :'[]'
32
+ def_delegator :@options, :'[]=', :'[]='
33
+ def_delegator :@options, :any?, :any?
34
+ def_delegator :@options, :each, :each
35
+ def_delegator :@options, :fetch, :fetch
36
+ def_delegator :@options, :to_a, :to_a
37
+ def_delegator :@options, :to_h, :to_h
38
+
39
+ ##
40
+ # @private
41
+ # @see Singleton
42
+ def initialize
43
+ @options = {}
44
+ end
45
+
46
+ ##
47
+ # @param opts [Hash]
48
+ # @option endpoint [String]
49
+ #
50
+ # @yield yields self to block
51
+ # @yieldparam config [Configuration] self
52
+ def configure!(**opts)
53
+ @options = opts
54
+ yield self if block_given?
55
+ self
56
+ end
57
+
58
+ ##
59
+ # Empties all configuration options
60
+ #
61
+ # @return opts [Hash]
62
+ def reset!
63
+ @options = {}
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ module Qa
3
+ module LDF
4
+ ##
5
+ # A search service that always returns no results.
6
+ #
7
+ # @example
8
+ # EmptySearchService.new.search('any query') # => {}
9
+ #
10
+ class EmptySearchService
11
+ ##
12
+ # @see Qa::Authority::Base#search
13
+ def search(_query)
14
+ []
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+ module Qa
3
+ module LDF
4
+ ##
5
+ # A customizable mapper from {RDF::Graph}s to JSON-like hashes for use in
6
+ # Questioning Authority.
7
+ #
8
+ # @todo: Add configuration
9
+ #
10
+ # @example maps a graphs to QA JSON objects.
11
+ # mapper = JsonMapper.new
12
+ #
13
+ # uri = 'http://id.loc.gov/authorities/subjects/sh2004002557'
14
+ # graph = RDF::Graph.load(uri)
15
+ #
16
+ # mapper.map_resource(uri, graph)
17
+ # # => { id: 'http://id.loc.gov/authorities/subjects/sh2004002557',
18
+ # # label: 'Marble Island (Nunavut)' }
19
+ #
20
+ class JsonMapper
21
+ ##
22
+ # @param uri [String] a URI-like string
23
+ # @param graph [RDF::Queryable]
24
+ #
25
+ # @return [Hash<Symbol, String>]
26
+ def map_resource(uri, graph)
27
+ labels =
28
+ graph.query(subject: RDF::URI.intern(uri),
29
+ predicate: RDF::Vocab::SKOS.prefLabel).objects
30
+
31
+ { id: uri, label: labels.first.to_s }
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,49 @@
1
+ require 'active_triples'
2
+
3
+ module Qa
4
+ module LDF
5
+ ##
6
+ # A base model for validatable authority values.
7
+ class Model
8
+ include ActiveTriples::RDFSource
9
+
10
+ class << self
11
+ ##
12
+ # Builds a model from the graph.
13
+ #
14
+ # @param graph [RDF::Graph]
15
+ #
16
+ # @return [Qa::LDF::Model] the built model
17
+ def from_graph(uri:, graph:)
18
+ new(uri) << graph
19
+ end
20
+
21
+ ##
22
+ # Builds a model from a QA result hash.
23
+ #
24
+ # @example
25
+ # model =
26
+ # Qa::LDF::Model.from_qa_result(id: 'http://example.com/moomin',
27
+ # label: 'Moomin'
28
+ # model.to_uri # => #<RDF::URI:0x... URI:http://example.com/moomin>
29
+ # model.rdf_label # => ['Moomin']
30
+ #
31
+ #
32
+ # @param qa_result [Hash<Symbol, String>]
33
+ #
34
+ # @return [Qa::LDF::Model] the built model
35
+ #
36
+ # @todo Make ActiveTriples::RDFSource#default_labels public or
37
+ # protected.
38
+ def from_qa_result(qa_result:)
39
+ qa_result.dup
40
+ model = new(qa_result.delete(:id))
41
+ model.set_value(model.send(:default_labels).first,
42
+ qa_result.delete(:label))
43
+
44
+ model
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,3 @@
1
+ require 'qa/ldf/spec/authority'
2
+ require 'qa/ldf/spec/model'
3
+ require 'qa/ldf/spec/search_service'
@@ -0,0 +1,112 @@
1
+ shared_examples 'an ldf authority' do
2
+ before do
3
+ unless defined?(authority)
4
+ raise ArgumentError,
5
+ "#{authority} must be defined with `let(:authority)` before " \
6
+ 'using LDF::Authority shared examples.'
7
+ end
8
+
9
+ unless defined?(ldf_uri)
10
+ raise ArgumentError,
11
+ "#{ldf_uri} must be defined with `let(:ldf_uri)` before using " \
12
+ 'LDF::Authority shared examples.'
13
+ end
14
+ unless defined?(ldf_label)
15
+ raise ArgumentError,
16
+ "#{ldf_label} must be defined with `let(:ldf_label)` before " \
17
+ 'using LDF::Authority shared examples.'
18
+ end
19
+ end
20
+
21
+ it 'is aliased to Qa::Authorities' do
22
+ unless described_class.eql?(Qa::LDF::Authority)
23
+ name = described_class.to_s.split('::').last.downcase.camelize
24
+ expect("Qa::Authorities::#{name}".constantize).to eql described_class
25
+ end
26
+ end
27
+
28
+ describe '#all' do
29
+ it 'is enumerable' do
30
+ expect(authority.all).to respond_to :each
31
+ end
32
+
33
+ it 'enumerates the vocabulary'
34
+ end
35
+
36
+ describe '#client' do
37
+ it 'defaults to an instance of DEFAULT_CLIENT' do
38
+ expect(described_class.new.client)
39
+ .to be_a described_class::DEFAULT_CLIENT
40
+ end
41
+
42
+ it 'sets to other instances' do
43
+ client = described_class::DEFAULT_CLIENT.new
44
+
45
+ expect { authority.client = client }
46
+ .to change { authority.client }
47
+ .to equal client
48
+ end
49
+ end
50
+
51
+ describe '#dataset' do
52
+ it 'defaults DEFAULT_DATASET_NAME' do
53
+ expect(described_class.new.dataset)
54
+ .to eq described_class::DEFAULT_DATASET_NAME
55
+ end
56
+
57
+ it 'sets to other symbols' do
58
+ dataset_name = :new_name
59
+
60
+ expect { authority.dataset = dataset_name }
61
+ .to change { authority.dataset }
62
+ .to eq dataset_name
63
+ end
64
+ end
65
+
66
+ describe '#find' do
67
+ it 'finds a uri' do
68
+ expect(authority.find(ldf_uri))
69
+ .to include id: ldf_uri.to_s, label: ldf_label
70
+ end
71
+
72
+ it 'maps to a json-friendly hash' do
73
+ expect { JSON.generate(authority.find(ldf_uri)) }.not_to raise_error
74
+ end
75
+ end
76
+
77
+ describe '#mapper' do
78
+ it 'defaults to an instance of DEFAULT_MAPPER' do
79
+ expect(described_class.new.mapper)
80
+ .to be_a described_class::DEFAULT_MAPPER
81
+ end
82
+
83
+ it 'sets to other instances' do
84
+ mapper = described_class::DEFAULT_MAPPER.new
85
+
86
+ expect { authority.mapper = mapper }
87
+ .to change { authority.mapper }
88
+ .to equal mapper
89
+ end
90
+ end
91
+
92
+ describe '#search' do
93
+ let(:query) { 'My Fake Query' }
94
+ let(:response) { { my: 'response' } }
95
+
96
+ it 'gives an array' do
97
+ expect(authority.search('tove')).to respond_to :to_ary
98
+ end
99
+
100
+ it 'gives a json-friendly array' do
101
+ expect { JSON.generate(authority.search('tove')) }.not_to raise_error
102
+ end
103
+
104
+ it 'searches the search service' do
105
+ authority.search_service = FakeSearchService.new do |service|
106
+ service.queries[query] = response
107
+ end
108
+
109
+ expect(authority.search(query)).to eq response
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,9 @@
1
+ shared_examples 'an ldf model' do
2
+ before do
3
+ unless defined?(model)
4
+ raise ArgumentError,
5
+ "#{model} must be defined with `let(:model)` before " \
6
+ 'using Qa::LDF::Model shared examples.'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,30 @@
1
+ shared_examples 'an ldf search service' do
2
+ before do
3
+ unless defined?(search_service)
4
+ raise ArgumentError,
5
+ "#{search_service} must be defined with `let(:search_service)` " \
6
+ 'before using LDF::Authority shared examples.'
7
+ end
8
+
9
+ unless defined?(searches)
10
+ warn 'You did not provide any valid searches to the ldf search service ' \
11
+ "shared examples for #{described_class}. Use `let(:searches) = " \
12
+ '{"query" => {key: "value"} to configure searches to test'
13
+ end
14
+ end
15
+
16
+ describe '#search' do
17
+ it 'gives a json-friendly array' do
18
+ JSON.generate(search_service.search('tove'))
19
+ expect { JSON.generate(search_service.search('tove')) }.not_to raise_error
20
+ end
21
+
22
+ it 'responds to searches' do
23
+ if defined?(searches)
24
+ searches.each do |query, response|
25
+ expect(search_service.search(query)).to eq response
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+ module Qa
3
+ module LDF
4
+ ##
5
+ # Gives the current version, parsed from `VERSION`.
6
+ #
7
+ # @example
8
+ # "The current version is: #{Qa::LDF::VERSION}"
9
+ #
10
+ module VERSION
11
+ FILE = File.expand_path('../../../../VERSION', __FILE__)
12
+ MAJOR, MINOR, TINY = File.read(FILE).chomp.split('.')
13
+ STRING = [MAJOR, MINOR, TINY].compact.join('.').freeze
14
+
15
+ ##
16
+ # @return [String]
17
+ def self.to_s
18
+ STRING
19
+ end
20
+
21
+ ##
22
+ # @return [String]
23
+ def self.to_str
24
+ STRING
25
+ end
26
+
27
+ ##
28
+ # @return [Array(String, String, String, String)]
29
+ def self.to_a
30
+ [MAJOR, MINOR, TINY].compact
31
+ end
32
+ end
33
+ end
34
+ end
data/qa-ldf.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env ruby -rubygems
2
+ # -*- encoding: utf-8 -*-
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.authors = ['Tom Johnson']
6
+ spec.email = ['tom@curationexperts.com']
7
+
8
+ spec.name = 'qa-ldf'
9
+ spec.homepage = 'http://github.com/curationexperts/qa-ldf'
10
+ spec.description = 'Questioning Authority cached with a Linked Data ' \
11
+ 'Fragment.'
12
+ spec.summary = 'Provides a bridge between questioning authority and ' \
13
+ 'a caching linked data fragment for URI authorities.'
14
+
15
+ spec.version = File.read('VERSION').chomp
16
+ spec.date = File.mtime('VERSION').strftime('%Y-%m-%d')
17
+ spec.license = 'Apache-2.0'
18
+
19
+ spec.files = `git ls-files`.split($OUTPUT_RECORD_SEPARATOR)
20
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
+ spec.require_paths = %w(lib)
22
+
23
+ spec.required_ruby_version = '>= 2.2.2'
24
+
25
+ spec.add_dependency 'qa', '~> 0.11.0'
26
+
27
+ spec.add_development_dependency 'guard', '~> 2.14'
28
+ spec.add_development_dependency 'ld_cache_fragment', '~> 0.1'
29
+ spec.add_development_dependency 'rake', '~> 12.0'
30
+ spec.add_development_dependency 'rspec', '~> 3.5'
31
+ spec.add_development_dependency 'rubocop-rspec', '1.10.0'
32
+ spec.add_development_dependency 'sham_rack', '~> 1.4'
33
+ spec.add_development_dependency 'webmock', '~> 2.3', '>= 2.3.2'
34
+ spec.add_development_dependency 'yard', '~> 0'
35
+ end