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