europeana-api 0.5.2 → 1.0.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.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.travis.yml +4 -2
- data/Gemfile +1 -8
- data/README.md +103 -21
- data/Rakefile +2 -1
- data/bin/console +21 -0
- data/europeana-api.gemspec +12 -1
- data/lib/europeana/api.rb +44 -68
- data/lib/europeana/api/annotation.rb +20 -0
- data/lib/europeana/api/client.rb +51 -0
- data/lib/europeana/api/entity.rb +16 -0
- data/lib/europeana/api/errors.rb +25 -41
- data/lib/europeana/api/faraday_middleware.rb +26 -0
- data/lib/europeana/api/faraday_middleware/request/authenticated_request.rb +25 -0
- data/lib/europeana/api/faraday_middleware/request/parameter_repetition.rb +25 -0
- data/lib/europeana/api/faraday_middleware/response/handle_text.rb +19 -0
- data/lib/europeana/api/faraday_middleware/response/parse_json_to_various.rb +52 -0
- data/lib/europeana/api/logger.rb +10 -0
- data/lib/europeana/api/queue.rb +47 -0
- data/lib/europeana/api/record.rb +29 -83
- data/lib/europeana/api/request.rb +77 -38
- data/lib/europeana/api/resource.rb +30 -0
- data/lib/europeana/api/response.rb +47 -0
- data/lib/europeana/api/version.rb +2 -1
- data/spec/europeana/api/annotation_spec.rb +77 -0
- data/spec/europeana/api/client_spec.rb +46 -0
- data/spec/europeana/api/entity_spec.rb +6 -0
- data/spec/europeana/api/faraday_middleware/request/authenticated_request_spec.rb +22 -0
- data/spec/europeana/api/queue_spec.rb +4 -0
- data/spec/europeana/api/record_spec.rb +54 -104
- data/spec/europeana/api/request_spec.rb +3 -0
- data/spec/europeana/api/resource_spec.rb +47 -0
- data/spec/europeana/api/response_spec.rb +10 -0
- data/spec/europeana/api_spec.rb +34 -84
- data/spec/spec_helper.rb +8 -0
- data/spec/support/shared_examples/resource_endpoint.rb +11 -0
- metadata +158 -34
- data/lib/europeana/api/record/hierarchy.rb +0 -48
- data/lib/europeana/api/record/hierarchy/ancestor_self_siblings.rb +0 -12
- data/lib/europeana/api/record/hierarchy/base.rb +0 -30
- data/lib/europeana/api/record/hierarchy/children.rb +0 -12
- data/lib/europeana/api/record/hierarchy/following_siblings.rb +0 -12
- data/lib/europeana/api/record/hierarchy/parent.rb +0 -12
- data/lib/europeana/api/record/hierarchy/preceding_siblings.rb +0 -15
- data/lib/europeana/api/record/hierarchy/self.rb +0 -12
- data/lib/europeana/api/requestable.rb +0 -118
- data/lib/europeana/api/search.rb +0 -64
- data/lib/europeana/api/search/fields.rb +0 -112
- data/spec/europeana/api/errors_spec.rb +0 -23
- data/spec/europeana/api/record/hierarchy_spec.rb +0 -15
- data/spec/europeana/api/search_spec.rb +0 -97
- data/spec/support/shared_examples/api_request.rb +0 -65
- data/spec/support/shared_examples/record_request.rb +0 -26
- data/spec/support/shared_examples/search_request.rb +0 -42
- data/spec/support/webmock.rb +0 -14
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'europeana/api/faraday_middleware'
|
3
|
+
require 'typhoeus/adapters/faraday'
|
4
|
+
|
5
|
+
module Europeana
|
6
|
+
module API
|
7
|
+
##
|
8
|
+
# The API client responsible for handling requests and responses
|
9
|
+
class Client
|
10
|
+
attr_reader :queue
|
11
|
+
|
12
|
+
delegate :get, :post, :put, :delete, :head, :patch, :options, to: :connection
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@queue = Queue.new(self)
|
16
|
+
end
|
17
|
+
|
18
|
+
def in_parallel?
|
19
|
+
@queue.present?
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# `Faraday` connection to the API
|
24
|
+
#
|
25
|
+
# * Requests are retried 5 times at an interval of 3 seconds
|
26
|
+
# * Requests are instrumented
|
27
|
+
# * JSON responses are parsed
|
28
|
+
#
|
29
|
+
# @return [Faraday::Connection]
|
30
|
+
def connection
|
31
|
+
@connection ||= begin
|
32
|
+
Faraday.new do |conn|
|
33
|
+
conn.request :instrumentation
|
34
|
+
conn.request :parameter_repetition
|
35
|
+
conn.request :authenticated_request
|
36
|
+
conn.request :retry, max: 5, interval: 3, exceptions: [Errno::ECONNREFUSED, EOFError]
|
37
|
+
|
38
|
+
conn.options.open_timeout = 5
|
39
|
+
conn.options.timeout = 45
|
40
|
+
|
41
|
+
conn.response :json_various, content_type: /\bjson$/
|
42
|
+
conn.response :text, content_type: %r{^text(\b[^/]+)?/(plain|html)$}
|
43
|
+
|
44
|
+
conn.adapter :typhoeus
|
45
|
+
conn.url_prefix = Europeana::API.url
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Europeana
|
3
|
+
module API
|
4
|
+
##
|
5
|
+
# Interface to the Entity API
|
6
|
+
#
|
7
|
+
# @see http://entity.europeana.eu/docs/
|
8
|
+
class Entity
|
9
|
+
include Resource
|
10
|
+
|
11
|
+
has_api_endpoint :resolve, path: '/entities/resolve'
|
12
|
+
has_api_endpoint :fetch, path: '/entities/%{type}/%{namespace}/%{identifier}'
|
13
|
+
has_api_endpoint :suggest, path: '/entities/suggest'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/europeana/api/errors.rb
CHANGED
@@ -1,60 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Europeana
|
2
3
|
module API
|
3
4
|
module Errors
|
4
|
-
|
5
|
-
|
6
|
-
#
|
7
|
-
# @todo Use one-line error messages (in backwards-incompatible version)
|
8
|
-
class MissingAPIKeyError < StandardError
|
9
|
-
def initialize(msg = nil)
|
10
|
-
msg ||= <<-MSG
|
11
|
-
Missing API key.
|
12
|
-
|
13
|
-
The Europeana API key has not been set.
|
14
|
-
|
15
|
-
Sign up for an API key at: http://labs.europeana.eu/api/registration/
|
16
|
-
|
17
|
-
Set the key with:
|
5
|
+
class Base < StandardError
|
6
|
+
attr_reader :faraday_response
|
18
7
|
|
19
|
-
|
20
|
-
|
21
|
-
super(msg)
|
8
|
+
def initialize(faraday_response)
|
9
|
+
@faraday_response = faraday_response
|
22
10
|
end
|
23
11
|
end
|
24
12
|
|
13
|
+
##
|
14
|
+
# Raised if API requests are attempted without the API key having been set.
|
15
|
+
class MissingAPIKeyError < Base
|
16
|
+
end
|
17
|
+
|
25
18
|
##
|
26
19
|
# Raised if the API response success flag is false, indicating a problem
|
27
20
|
# with the request.
|
28
|
-
class RequestError <
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
21
|
+
class RequestError < Base
|
22
|
+
end
|
23
|
+
|
24
|
+
class ResourceNotFoundError < Base
|
25
|
+
end
|
26
|
+
|
27
|
+
class ClientError < Base
|
28
|
+
end
|
29
|
+
|
30
|
+
class ServerError < Base
|
37
31
|
end
|
38
32
|
|
39
33
|
##
|
40
34
|
# Raised if the API response is not valid JSON.
|
41
|
-
class ResponseError <
|
42
|
-
def initialize(msg = nil)
|
43
|
-
msg ||= <<-MSG
|
44
|
-
Response error.
|
45
|
-
|
46
|
-
Unable to parse the response from the Europeana API.
|
47
|
-
MSG
|
48
|
-
super(msg)
|
49
|
-
end
|
35
|
+
class ResponseError < Base
|
50
36
|
end
|
51
37
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
class PaginationError < StandardError
|
57
|
-
end
|
38
|
+
##
|
39
|
+
# Raised if the API response indicates invalid pagination params in
|
40
|
+
# the request.
|
41
|
+
class PaginationError < Base
|
58
42
|
end
|
59
43
|
end
|
60
44
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'faraday'
|
4
|
+
require 'faraday_middleware'
|
5
|
+
|
6
|
+
module Europeana
|
7
|
+
module API
|
8
|
+
##
|
9
|
+
# `Faraday` middleware for Europeana API specific behaviours
|
10
|
+
module FaradayMiddleware
|
11
|
+
autoload :AuthenticatedRequest, 'europeana/api/faraday_middleware/request/authenticated_request'
|
12
|
+
autoload :ParameterRepetition, 'europeana/api/faraday_middleware/request/parameter_repetition'
|
13
|
+
|
14
|
+
autoload :HandleText, 'europeana/api/faraday_middleware/response/handle_text'
|
15
|
+
autoload :ParseJsonToVarious, 'europeana/api/faraday_middleware/response/parse_json_to_various'
|
16
|
+
|
17
|
+
Faraday::Request.register_middleware \
|
18
|
+
authenticated_request: -> { AuthenticatedRequest },
|
19
|
+
parameter_repetition: -> { ParameterRepetition }
|
20
|
+
|
21
|
+
Faraday::Response.register_middleware \
|
22
|
+
text: -> { HandleText },
|
23
|
+
json_various: -> { ParseJsonToVarious }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'rack'
|
3
|
+
|
4
|
+
module Europeana
|
5
|
+
module API
|
6
|
+
module FaradayMiddleware
|
7
|
+
##
|
8
|
+
# `Faraday` middleware to handle Europeana API authentication
|
9
|
+
class AuthenticatedRequest < Faraday::Middleware
|
10
|
+
def call(env)
|
11
|
+
ensure_api_key(env)
|
12
|
+
@app.call env
|
13
|
+
end
|
14
|
+
|
15
|
+
def ensure_api_key(env)
|
16
|
+
query = Rack::Utils.parse_query(env.url.query)
|
17
|
+
return if query.key?('wskey')
|
18
|
+
|
19
|
+
query['wskey'] = Europeana::API.key
|
20
|
+
env.url.query = Rack::Utils.build_query(query)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Europeana
|
3
|
+
module API
|
4
|
+
module FaradayMiddleware
|
5
|
+
##
|
6
|
+
# Handles multiple URL params with the same name
|
7
|
+
#
|
8
|
+
# Europeana's APIs expect params with the same name to appear in URL
|
9
|
+
# queries like `?qf=this&qf=that`, but Faraday constructs them the
|
10
|
+
# Rack/Rails way, like `?qf[]=this&qf[]=that`. This middleware constructs
|
11
|
+
# them to the former format, using `Rack::Utils.build_query`.
|
12
|
+
class ParameterRepetition < Faraday::Middleware
|
13
|
+
def call(env)
|
14
|
+
repeat_query_parameters(env)
|
15
|
+
@app.call env
|
16
|
+
end
|
17
|
+
|
18
|
+
def repeat_query_parameters(env)
|
19
|
+
query = Rack::Utils.parse_nested_query(env.url.query)
|
20
|
+
env.url.query = Rack::Utils.build_query(query)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'faraday_middleware/response_middleware'
|
3
|
+
|
4
|
+
module Europeana
|
5
|
+
module API
|
6
|
+
module FaradayMiddleware
|
7
|
+
##
|
8
|
+
# Handles plain text & HTML responses from the API, which are never desired
|
9
|
+
class HandleText < ::FaradayMiddleware::ResponseMiddleware
|
10
|
+
def process_response(env)
|
11
|
+
super
|
12
|
+
content_type = env.response_headers['Content-Type']
|
13
|
+
fail Europeana::API::Errors::ResponseError.new(env),
|
14
|
+
%(API responded with Content-Type "#{content_type}" and status #{env[:status]})
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'faraday_middleware/response_middleware'
|
3
|
+
require 'active_support/json'
|
4
|
+
|
5
|
+
module Europeana
|
6
|
+
module API
|
7
|
+
module FaradayMiddleware
|
8
|
+
##
|
9
|
+
# Handles JSON parsing of API responses
|
10
|
+
#
|
11
|
+
# Returns the response as either a `Hash` (default) or an `OpenStruct`.
|
12
|
+
#
|
13
|
+
# To set the response format to be `OpenStruct`:
|
14
|
+
# ```ruby
|
15
|
+
# Europeana::API.configure do |config|
|
16
|
+
# config.parse_json_to = OpenStruct
|
17
|
+
# end
|
18
|
+
# ```
|
19
|
+
#
|
20
|
+
# If using `OpenStruct`, changes "-" in JSON field keys to "_", so that
|
21
|
+
# they become methods.
|
22
|
+
class ParseJsonToVarious < ::FaradayMiddleware::ResponseMiddleware
|
23
|
+
dependency do
|
24
|
+
require 'json' unless defined?(JSON)
|
25
|
+
end
|
26
|
+
|
27
|
+
define_parser do |body|
|
28
|
+
unless body.strip.empty?
|
29
|
+
hash = JSON.parse(body)
|
30
|
+
if Europeana::API.configuration.parse_json_to == OpenStruct
|
31
|
+
underscored_hash = underscore_hash_keys(hash)
|
32
|
+
underscored_body = underscored_hash.to_json
|
33
|
+
JSON.parse(underscored_body, object_class: OpenStruct)
|
34
|
+
else
|
35
|
+
hash.with_indifferent_access
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class << self
|
41
|
+
def underscore_hash_keys(hash)
|
42
|
+
hash.keys.each do |k|
|
43
|
+
hash[k] = underscore_hash_keys(hash[k]) if hash[k].is_a?(Hash)
|
44
|
+
hash[k.underscore] = hash.delete(k) if k =~ /-/
|
45
|
+
end
|
46
|
+
hash
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'active_support/notifications'
|
3
|
+
|
4
|
+
# Subscribe to Faraday request instrumentation
|
5
|
+
ActiveSupport::Notifications.subscribe('request.faraday') do |_name, starts, ends, _id, env|
|
6
|
+
url = env[:url]
|
7
|
+
http_method = env[:method].to_s.upcase
|
8
|
+
duration = ends - starts
|
9
|
+
Europeana::API.logger.info(format('%s %s (%.3f s)', http_method, url, duration))
|
10
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Europeana
|
3
|
+
module API
|
4
|
+
##
|
5
|
+
# A queue of API requests to run in parallel
|
6
|
+
class Queue
|
7
|
+
Item = Struct.new(:resource, :method, :params)
|
8
|
+
|
9
|
+
attr_reader :client
|
10
|
+
|
11
|
+
def initialize(client)
|
12
|
+
@client = client
|
13
|
+
@items = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def present?
|
17
|
+
@items.present?
|
18
|
+
end
|
19
|
+
|
20
|
+
def add(resource, method, **params)
|
21
|
+
@items << Item.new(resource, method, **params)
|
22
|
+
end
|
23
|
+
|
24
|
+
def run
|
25
|
+
responses = []
|
26
|
+
|
27
|
+
client.connection.in_parallel do
|
28
|
+
@items.each do |item|
|
29
|
+
resource_class = Europeana::API.send(item.resource)
|
30
|
+
request = resource_class.send(:api_request_for_endpoint, item.method, item.params)
|
31
|
+
request.client = client
|
32
|
+
responses << request.execute
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
responses.map do |response|
|
37
|
+
begin
|
38
|
+
response.validate!
|
39
|
+
response.body
|
40
|
+
rescue StandardError => exception
|
41
|
+
exception
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/europeana/api/record.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Europeana
|
2
3
|
module API
|
3
4
|
##
|
@@ -5,93 +6,38 @@ module Europeana
|
|
5
6
|
#
|
6
7
|
# @see http://labs.europeana.eu/api/record/
|
7
8
|
class Record
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
def initialize(id, params = {})
|
22
|
-
self.request_params = params
|
23
|
-
@id = id
|
24
|
-
end
|
25
|
-
|
26
|
-
##
|
27
|
-
# Sets record ID attribute after validating format.
|
28
|
-
#
|
29
|
-
# @param [String] id Record ID
|
30
|
-
#
|
31
|
-
def id=(id)
|
32
|
-
unless id.is_a?(String) && id.match(%r{\A/[^/]+/[^/]+\B})
|
33
|
-
fail ArgumentError, "Invalid Europeana record ID: \"#{id}\""
|
34
|
-
end
|
35
|
-
@id = id
|
36
|
-
end
|
37
|
-
|
38
|
-
##
|
39
|
-
# Sets request parameters after validating keys
|
40
|
-
#
|
41
|
-
# Valid parameter keys:
|
42
|
-
# * :callback
|
43
|
-
#
|
44
|
-
# For explanations of these request parameters, see: http://labs.europeana.eu/api/record/
|
45
|
-
#
|
46
|
-
# @param (see #initialize)
|
47
|
-
# @return [Hash] Request parameters
|
48
|
-
def params=(params = {})
|
49
|
-
params.assert_valid_keys(:callback, :wskey)
|
50
|
-
@params = params
|
51
|
-
end
|
52
|
-
|
53
|
-
##
|
54
|
-
# Gets the URL for this Record request
|
55
|
-
#
|
56
|
-
# @param [Hash{Symbol => Object}] options
|
57
|
-
# @option options [Boolean] :ld (false)
|
58
|
-
# Request JSON-LD
|
59
|
-
# @return [String]
|
60
|
-
def request_url(options = {})
|
61
|
-
options.assert_valid_keys(:ld)
|
62
|
-
(api_url + "/record#{@id}.json").tap do |url|
|
63
|
-
url << 'ld' if options[:ld]
|
64
|
-
end
|
9
|
+
include Resource
|
10
|
+
|
11
|
+
has_api_endpoint :search,
|
12
|
+
path: '/v2/search.json',
|
13
|
+
errors: {
|
14
|
+
'Invalid query parameter' => Errors::RequestError,
|
15
|
+
/1000 search results/ => Errors::PaginationError
|
16
|
+
}
|
17
|
+
has_api_endpoint :fetch, path: '/v2/record%{id}.json'
|
18
|
+
|
19
|
+
# Hierarchies
|
20
|
+
%w(self parent children preceding_siblings following_siblings ancestor_self_siblings).each do |hierarchical|
|
21
|
+
has_api_endpoint hierarchical.to_sym, path: "/v2/record%{id}/#{hierarchical.dasherize}.json"
|
65
22
|
end
|
66
23
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
24
|
+
class << self
|
25
|
+
##
|
26
|
+
# Escapes Lucene syntax special characters for use in query parameters.
|
27
|
+
#
|
28
|
+
# The `Europeana::API` gem does not perform this escaping itself.
|
29
|
+
# Applications using the gem are responsible for escaping parameters
|
30
|
+
# when needed.
|
31
|
+
#
|
32
|
+
# @param [String] text Text to escape
|
33
|
+
# @return [String] Escaped text
|
34
|
+
def escape(text)
|
35
|
+
fail ArgumentError, "Expected String, got #{text.class}" unless text.is_a?(String)
|
36
|
+
specials = %w<\\ + - & | ! ( ) { } [ ] ^ " ~ * ? : />
|
37
|
+
specials.each_with_object(text.dup) do |char, unescaped|
|
38
|
+
unescaped.gsub!(char, '\\\\' + char) # prepends *one* backslash
|
81
39
|
end
|
82
40
|
end
|
83
|
-
rescue JSON::ParserError
|
84
|
-
if response.code.to_i == 404
|
85
|
-
# Handle HTML 404 responses on malformed record ID, emulating API's
|
86
|
-
# JSON response.
|
87
|
-
raise Errors::RequestError, "Invalid record identifier: #{@id}"
|
88
|
-
else
|
89
|
-
raise
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
def hierarchy
|
94
|
-
@hierarchy ||= Hierarchy.new(id)
|
95
41
|
end
|
96
42
|
end
|
97
43
|
end
|