publishing_platform_api_adapters 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +2 -0
- data/Rakefile +8 -0
- data/lib/publishing_platform_api/base.rb +87 -0
- data/lib/publishing_platform_api/exceptions.rb +116 -0
- data/lib/publishing_platform_api/json_client.rb +201 -0
- data/lib/publishing_platform_api/list_response.rb +89 -0
- data/lib/publishing_platform_api/middleware/publishing_platform_header_sniffer.rb +21 -0
- data/lib/publishing_platform_api/publishing_api.rb +222 -0
- data/lib/publishing_platform_api/publishing_platform_headers.rb +23 -0
- data/lib/publishing_platform_api/railtie.rb +30 -0
- data/lib/publishing_platform_api/response.rb +183 -0
- data/lib/publishing_platform_api/version.rb +5 -0
- data/lib/publishing_platform_api.rb +20 -0
- data/lib/publishing_platform_api_adapters.rb +3 -0
- metadata +140 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9b05ed814403c8e76bb61a31e28e271636a959b364b91790c4db66480814d726
|
4
|
+
data.tar.gz: a6c23beead962d8c2db6830e3022d7876d783a917fc6d8b468ac00fcd07fd5bb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: '06587180c50b7e88b24df754c3ace7d7d7202182d28d066736cba648f57260f46c86fa7d1ecb9e5ce5057ee00f3bc7b48f1530cf375161261ae8b621b375edd7'
|
7
|
+
data.tar.gz: 6dbd74d1d29c65369b5f605d29af19bc6404c901e55164997a70b3b4d74dfe57ad7d06d7e3477e2ae5f08600f366f8ad054902d3d54af9ae99080e3ac40cab12
|
data/README.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require_relative "json_client"
|
2
|
+
require "cgi"
|
3
|
+
require "null_logger"
|
4
|
+
require "publishing_platform_location"
|
5
|
+
require_relative "list_response"
|
6
|
+
|
7
|
+
class PublishingPlatformApi::Base
|
8
|
+
class InvalidAPIURL < StandardError
|
9
|
+
end
|
10
|
+
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
def client
|
14
|
+
@client ||= create_client
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_client
|
18
|
+
PublishingPlatformApi::JsonClient.new(options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def_delegators :client,
|
22
|
+
:get_json,
|
23
|
+
:post_json,
|
24
|
+
:put_json,
|
25
|
+
:patch_json,
|
26
|
+
:delete_json,
|
27
|
+
:get_raw,
|
28
|
+
:get_raw!,
|
29
|
+
:put_multipart,
|
30
|
+
:post_multipart
|
31
|
+
|
32
|
+
attr_reader :options
|
33
|
+
|
34
|
+
class << self
|
35
|
+
attr_writer :logger
|
36
|
+
attr_accessor :default_options
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.logger
|
40
|
+
@logger ||= NullLogger.instance
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(endpoint_url, options = {})
|
44
|
+
options[:endpoint_url] = endpoint_url
|
45
|
+
raise InvalidAPIURL unless endpoint_url =~ URI::RFC3986_Parser::RFC3986_URI
|
46
|
+
|
47
|
+
base_options = { logger: PublishingPlatformApi::Base.logger }
|
48
|
+
default_options = base_options.merge(PublishingPlatformApi::Base.default_options || {})
|
49
|
+
@options = default_options.merge(options)
|
50
|
+
self.endpoint = options[:endpoint_url]
|
51
|
+
end
|
52
|
+
|
53
|
+
def url_for_slug(slug, options = {})
|
54
|
+
"#{base_url}/#{slug}.json#{query_string(options)}"
|
55
|
+
end
|
56
|
+
|
57
|
+
def get_list(url)
|
58
|
+
get_json(url) do |r|
|
59
|
+
PublishingPlatformApi::ListResponse.new(r, self)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
attr_accessor :endpoint
|
66
|
+
|
67
|
+
def query_string(params)
|
68
|
+
return "" if params.empty?
|
69
|
+
|
70
|
+
param_pairs = params.sort.map { |key, value|
|
71
|
+
case value
|
72
|
+
when Array
|
73
|
+
value.map do |v|
|
74
|
+
"#{CGI.escape("#{key}[]")}=#{CGI.escape(v.to_s)}"
|
75
|
+
end
|
76
|
+
else
|
77
|
+
"#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
|
78
|
+
end
|
79
|
+
}.flatten
|
80
|
+
|
81
|
+
"?#{param_pairs.join('&')}"
|
82
|
+
end
|
83
|
+
|
84
|
+
def uri_encode(param)
|
85
|
+
Addressable::URI.encode(param.to_s)
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module PublishingPlatformApi
|
2
|
+
# Abstract error class
|
3
|
+
class BaseError < StandardError
|
4
|
+
# Give Sentry extra context about this event
|
5
|
+
# https://docs.sentry.io/clients/ruby/context/
|
6
|
+
def sentry_context
|
7
|
+
{
|
8
|
+
# Make Sentry group exceptions by type instead of message, so all
|
9
|
+
# exceptions like `PublishingPlatformApi::TimedOutException` will get grouped as one
|
10
|
+
# error and not an error per URL.
|
11
|
+
fingerprint: [self.class.name],
|
12
|
+
}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class EndpointNotFound < BaseError; end
|
17
|
+
|
18
|
+
class TimedOutException < BaseError; end
|
19
|
+
|
20
|
+
class InvalidUrl < BaseError; end
|
21
|
+
|
22
|
+
class SocketErrorException < BaseError; end
|
23
|
+
|
24
|
+
# Superclass for all 4XX and 5XX errors
|
25
|
+
class HTTPErrorResponse < BaseError
|
26
|
+
attr_accessor :code, :error_details, :http_body
|
27
|
+
|
28
|
+
def initialize(code, message = nil, error_details = nil, http_body = nil)
|
29
|
+
super(message)
|
30
|
+
@code = code
|
31
|
+
@error_details = error_details
|
32
|
+
@http_body = http_body
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Superclass & fallback for all 4XX errors
|
37
|
+
class HTTPClientError < HTTPErrorResponse; end
|
38
|
+
|
39
|
+
class HTTPIntermittentClientError < HTTPClientError; end
|
40
|
+
|
41
|
+
class HTTPNotFound < HTTPClientError; end
|
42
|
+
|
43
|
+
class HTTPGone < HTTPClientError; end
|
44
|
+
|
45
|
+
class HTTPPayloadTooLarge < HTTPClientError; end
|
46
|
+
|
47
|
+
class HTTPUnauthorized < HTTPClientError; end
|
48
|
+
|
49
|
+
class HTTPForbidden < HTTPClientError; end
|
50
|
+
|
51
|
+
class HTTPConflict < HTTPClientError; end
|
52
|
+
|
53
|
+
class HTTPUnprocessableEntity < HTTPClientError; end
|
54
|
+
|
55
|
+
class HTTPBadRequest < HTTPClientError; end
|
56
|
+
|
57
|
+
class HTTPTooManyRequests < HTTPIntermittentClientError; end
|
58
|
+
|
59
|
+
# Superclass & fallback for all 5XX errors
|
60
|
+
class HTTPServerError < HTTPErrorResponse; end
|
61
|
+
|
62
|
+
class HTTPIntermittentServerError < HTTPServerError; end
|
63
|
+
|
64
|
+
class HTTPInternalServerError < HTTPServerError; end
|
65
|
+
|
66
|
+
class HTTPBadGateway < HTTPIntermittentServerError; end
|
67
|
+
|
68
|
+
class HTTPUnavailable < HTTPIntermittentServerError; end
|
69
|
+
|
70
|
+
class HTTPGatewayTimeout < HTTPIntermittentServerError; end
|
71
|
+
|
72
|
+
module ExceptionHandling
|
73
|
+
def build_specific_http_error(error, url, details = nil)
|
74
|
+
message = "URL: #{url}\nResponse body:\n#{error.http_body}"
|
75
|
+
code = error.http_code
|
76
|
+
error_class_for_code(code).new(code, message, details, error.http_body)
|
77
|
+
end
|
78
|
+
|
79
|
+
def error_class_for_code(code)
|
80
|
+
case code
|
81
|
+
when 400
|
82
|
+
PublishingPlatformApi::HTTPBadRequest
|
83
|
+
when 401
|
84
|
+
PublishingPlatformApi::HTTPUnauthorized
|
85
|
+
when 403
|
86
|
+
PublishingPlatformApi::HTTPForbidden
|
87
|
+
when 404
|
88
|
+
PublishingPlatformApi::HTTPNotFound
|
89
|
+
when 409
|
90
|
+
PublishingPlatformApi::HTTPConflict
|
91
|
+
when 410
|
92
|
+
PublishingPlatformApi::HTTPGone
|
93
|
+
when 413
|
94
|
+
PublishingPlatformApi::HTTPPayloadTooLarge
|
95
|
+
when 422
|
96
|
+
PublishingPlatformApi::HTTPUnprocessableEntity
|
97
|
+
when 429
|
98
|
+
PublishingPlatformApi::HTTPTooManyRequests
|
99
|
+
when (400..499)
|
100
|
+
PublishingPlatformApi::HTTPClientError
|
101
|
+
when 500
|
102
|
+
PublishingPlatformApi::HTTPInternalServerError
|
103
|
+
when 502
|
104
|
+
PublishingPlatformApi::HTTPBadGateway
|
105
|
+
when 503
|
106
|
+
PublishingPlatformApi::HTTPUnavailable
|
107
|
+
when 504
|
108
|
+
PublishingPlatformApi::HTTPGatewayTimeout
|
109
|
+
when (500..599)
|
110
|
+
PublishingPlatformApi::HTTPServerError
|
111
|
+
else
|
112
|
+
PublishingPlatformApi::HTTPErrorResponse
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
require_relative "response"
|
2
|
+
require_relative "exceptions"
|
3
|
+
require_relative "version"
|
4
|
+
require_relative "publishing_platform_headers"
|
5
|
+
require "rest-client"
|
6
|
+
require "null_logger"
|
7
|
+
|
8
|
+
module PublishingPlatformApi
|
9
|
+
class JsonClient
|
10
|
+
include PublishingPlatformApi::ExceptionHandling
|
11
|
+
|
12
|
+
attr_accessor :logger, :options
|
13
|
+
|
14
|
+
def initialize(options = {})
|
15
|
+
if options[:disable_timeout] || options[:timeout].to_i.negative?
|
16
|
+
raise "It is no longer possible to disable the timeout."
|
17
|
+
end
|
18
|
+
|
19
|
+
@logger = options[:logger] || NullLogger.instance
|
20
|
+
@options = options
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.default_request_headers
|
24
|
+
{
|
25
|
+
"Accept" => "application/json",
|
26
|
+
# PUBLISHING_PLATFORM_APP_NAME is set for all apps
|
27
|
+
"User-Agent" => "publishing_platform_api_adapters/#{PublishingPlatformApi::VERSION} (#{ENV['PUBLISHING_PLATFORM_APP_NAME']})",
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.default_request_with_json_body_headers
|
32
|
+
default_request_headers.merge(json_body_headers)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.json_body_headers
|
36
|
+
{
|
37
|
+
"Content-Type" => "application/json",
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
DEFAULT_TIMEOUT_IN_SECONDS = 4
|
42
|
+
|
43
|
+
def get_raw!(url)
|
44
|
+
do_raw_request(:get, url)
|
45
|
+
end
|
46
|
+
|
47
|
+
def get_raw(url)
|
48
|
+
get_raw!(url)
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_json(url, additional_headers = {}, &create_response)
|
52
|
+
do_json_request(:get, url, nil, additional_headers, &create_response)
|
53
|
+
end
|
54
|
+
|
55
|
+
def post_json(url, params = {}, additional_headers = {})
|
56
|
+
do_json_request(:post, url, params, additional_headers)
|
57
|
+
end
|
58
|
+
|
59
|
+
def put_json(url, params, additional_headers = {})
|
60
|
+
do_json_request(:put, url, params, additional_headers)
|
61
|
+
end
|
62
|
+
|
63
|
+
def patch_json(url, params, additional_headers = {})
|
64
|
+
do_json_request(:patch, url, params, additional_headers)
|
65
|
+
end
|
66
|
+
|
67
|
+
def delete_json(url, params = {}, additional_headers = {})
|
68
|
+
do_json_request(:delete, url, params, additional_headers)
|
69
|
+
end
|
70
|
+
|
71
|
+
def post_multipart(url, params)
|
72
|
+
r = do_raw_request(:post, url, params.merge(multipart: true))
|
73
|
+
Response.new(r)
|
74
|
+
end
|
75
|
+
|
76
|
+
def put_multipart(url, params)
|
77
|
+
r = do_raw_request(:put, url, params.merge(multipart: true))
|
78
|
+
Response.new(r)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def do_raw_request(method, url, params = nil)
|
84
|
+
do_request(method, url, params)
|
85
|
+
rescue RestClient::Exception => e
|
86
|
+
raise build_specific_http_error(e, url, nil)
|
87
|
+
end
|
88
|
+
|
89
|
+
# method: the symbolic name of the method to use, e.g. :get, :post
|
90
|
+
# url: the request URL
|
91
|
+
# params: the data to send (JSON-serialised) in the request body
|
92
|
+
# additional_headers: headers to set on the request (in addition to the default ones)
|
93
|
+
# create_response: optional block to instantiate a custom response object
|
94
|
+
# from the Net::HTTPResponse
|
95
|
+
def do_json_request(method, url, params = nil, additional_headers = {}, &create_response)
|
96
|
+
begin
|
97
|
+
if params
|
98
|
+
additional_headers.merge!(self.class.json_body_headers)
|
99
|
+
end
|
100
|
+
response = do_request(method, url, (params.to_json if params), additional_headers)
|
101
|
+
rescue RestClient::Exception => e
|
102
|
+
# Attempt to parse the body as JSON if possible
|
103
|
+
error_details = begin
|
104
|
+
e.http_body ? JSON.parse(e.http_body) : nil
|
105
|
+
rescue JSON::ParserError
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
raise build_specific_http_error(e, url, error_details)
|
109
|
+
end
|
110
|
+
|
111
|
+
# If no custom response is given, just instantiate Response
|
112
|
+
create_response ||= proc { |r| Response.new(r) }
|
113
|
+
create_response.call(response)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Take a hash of parameters for Request#execute; return a hash of
|
117
|
+
# parameters with authentication information included
|
118
|
+
def with_auth_options(method_params)
|
119
|
+
if @options[:bearer_token]
|
120
|
+
headers = method_params[:headers] || {}
|
121
|
+
method_params.merge(headers: headers.merge(
|
122
|
+
"Authorization" => "Bearer #{@options[:bearer_token]}",
|
123
|
+
))
|
124
|
+
elsif @options[:basic_auth]
|
125
|
+
method_params.merge(
|
126
|
+
user: @options[:basic_auth][:user],
|
127
|
+
password: @options[:basic_auth][:password],
|
128
|
+
)
|
129
|
+
else
|
130
|
+
method_params
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Take a hash of parameters for Request#execute; return a hash of
|
135
|
+
# parameters with timeouts included
|
136
|
+
def with_timeout(method_params)
|
137
|
+
method_params.merge(
|
138
|
+
timeout: options[:timeout] || DEFAULT_TIMEOUT_IN_SECONDS,
|
139
|
+
open_timeout: options[:timeout] || DEFAULT_TIMEOUT_IN_SECONDS,
|
140
|
+
)
|
141
|
+
end
|
142
|
+
|
143
|
+
def with_headers(method_params, default_headers, additional_headers)
|
144
|
+
method_params.merge(
|
145
|
+
headers: default_headers
|
146
|
+
.merge(method_params[:headers] || {})
|
147
|
+
.merge(PublishingPlatformApi::PublishingPlatformHeaders.headers)
|
148
|
+
.merge(additional_headers),
|
149
|
+
)
|
150
|
+
end
|
151
|
+
|
152
|
+
def with_ssl_options(method_params)
|
153
|
+
method_params.merge(
|
154
|
+
# This is the default value anyway, but we should probably be explicit
|
155
|
+
verify_ssl: OpenSSL::SSL::VERIFY_NONE,
|
156
|
+
)
|
157
|
+
end
|
158
|
+
|
159
|
+
def do_request(method, url, params = nil, additional_headers = {})
|
160
|
+
loggable = { request_uri: url, start_time: Time.now.to_f, publishing_platform_request_id: PublishingPlatformApi::PublishingPlatformHeaders.headers[:publishing_platform_request_id] }.compact
|
161
|
+
start_logging = loggable.merge(action: "start")
|
162
|
+
logger.debug start_logging.to_json
|
163
|
+
|
164
|
+
method_params = {
|
165
|
+
method:,
|
166
|
+
url:,
|
167
|
+
}
|
168
|
+
|
169
|
+
method_params[:payload] = params
|
170
|
+
method_params = with_timeout(method_params)
|
171
|
+
method_params = with_headers(method_params, self.class.default_request_headers, additional_headers)
|
172
|
+
method_params = with_auth_options(method_params)
|
173
|
+
if URI.parse(url).is_a? URI::HTTPS
|
174
|
+
method_params = with_ssl_options(method_params)
|
175
|
+
end
|
176
|
+
|
177
|
+
::RestClient::Request.execute(method_params)
|
178
|
+
rescue Errno::ECONNREFUSED => e
|
179
|
+
logger.error loggable.merge(status: "refused", error_message: e.message, error_class: e.class.name, end_time: Time.now.to_f).to_json
|
180
|
+
raise PublishingPlatformApi::EndpointNotFound, "Could not connect to #{url}"
|
181
|
+
rescue RestClient::Exceptions::Timeout => e
|
182
|
+
logger.error loggable.merge(status: "timeout", error_message: e.message, error_class: e.class.name, end_time: Time.now.to_f).to_json
|
183
|
+
raise PublishingPlatformApi::TimedOutException, e.message
|
184
|
+
rescue URI::InvalidURIError => e
|
185
|
+
logger.error loggable.merge(status: "invalid_uri", error_message: e.message, error_class: e.class.name, end_time: Time.now.to_f).to_json
|
186
|
+
raise PublishingPlatformApi::InvalidUrl, e.message
|
187
|
+
rescue RestClient::Exception => e
|
188
|
+
# Log the error here, since we have access to loggable, but raise the
|
189
|
+
# exception up to the calling method to deal with
|
190
|
+
loggable.merge!(status: e.http_code, end_time: Time.now.to_f, body: e.http_body)
|
191
|
+
logger.warn loggable.to_json
|
192
|
+
raise
|
193
|
+
rescue Errno::ECONNRESET => e
|
194
|
+
logger.error loggable.merge(status: "connection_reset", error_message: e.message, error_class: e.class.name, end_time: Time.now.to_f).to_json
|
195
|
+
raise PublishingPlatformApi::TimedOutException, e.message
|
196
|
+
rescue SocketError => e
|
197
|
+
logger.error loggable.merge(status: "socket_error", error_message: e.message, error_class: e.class.name, end_time: Time.now.to_f).to_json
|
198
|
+
raise PublishingPlatformApi::SocketErrorException, e.message
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require "json"
|
2
|
+
require "publishing_platform_api/response"
|
3
|
+
require "link_header"
|
4
|
+
|
5
|
+
module PublishingPlatformApi
|
6
|
+
# Response class for lists of multiple items.
|
7
|
+
#
|
8
|
+
# This expects responses to be in a common format, with the list of results
|
9
|
+
# contained under the `results` key. The response may also have previous and
|
10
|
+
# subsequent pages, indicated by entries in the response's `Link` header.
|
11
|
+
class ListResponse < Response
|
12
|
+
# The ListResponse is instantiated with a reference back to the API client,
|
13
|
+
# so it can make requests for the subsequent pages
|
14
|
+
def initialize(response, api_client, options = {})
|
15
|
+
super(response, options)
|
16
|
+
@api_client = api_client
|
17
|
+
end
|
18
|
+
|
19
|
+
# Pass calls to `self.each` to the `results` sub-object, so we can iterate
|
20
|
+
# over the response directly
|
21
|
+
def_delegators :results, :each, :to_ary
|
22
|
+
|
23
|
+
def results
|
24
|
+
to_hash["results"]
|
25
|
+
end
|
26
|
+
|
27
|
+
def has_next_page?
|
28
|
+
!page_link("next").nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
def next_page
|
32
|
+
# This shouldn't be a performance problem, since the cache will generally
|
33
|
+
# avoid us making multiple requests for the same page, but we shouldn't
|
34
|
+
# allow the data to change once it's already been loaded, so long as we
|
35
|
+
# retain a reference to any one page in the sequence
|
36
|
+
@next_page ||= if has_next_page?
|
37
|
+
@api_client.get_list page_link("next").href
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def has_previous_page?
|
42
|
+
!page_link("previous").nil?
|
43
|
+
end
|
44
|
+
|
45
|
+
def previous_page
|
46
|
+
# See the note in `next_page` for why this is memoised
|
47
|
+
@previous_page ||= if has_previous_page?
|
48
|
+
@api_client.get_list(page_link("previous").href)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Transparently get all results across all pages. Compare this with #each
|
53
|
+
# or #results which only iterate over the current page.
|
54
|
+
#
|
55
|
+
# Example:
|
56
|
+
#
|
57
|
+
# list_response.with_subsequent_pages.each do |result|
|
58
|
+
# ...
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# or:
|
62
|
+
#
|
63
|
+
# list_response.with_subsequent_pages.count
|
64
|
+
#
|
65
|
+
# Pages of results are fetched on demand. When iterating, that means
|
66
|
+
# fetching pages as results from the current page are exhausted. If you
|
67
|
+
# invoke a method such as #count, this method will fetch all pages at that
|
68
|
+
# point. Note that the responses are stored so subsequent pages will not be
|
69
|
+
# loaded multiple times.
|
70
|
+
def with_subsequent_pages
|
71
|
+
Enumerator.new do |yielder|
|
72
|
+
each { |i| yielder << i }
|
73
|
+
if has_next_page?
|
74
|
+
next_page.with_subsequent_pages.each { |i| yielder << i }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def link_header
|
82
|
+
@link_header ||= LinkHeader.parse @http_response.headers[:link]
|
83
|
+
end
|
84
|
+
|
85
|
+
def page_link(rel)
|
86
|
+
link_header.find_link(["rel", rel])
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require_relative "../publishing_platform_headers"
|
2
|
+
|
3
|
+
module PublishingPlatformApi
|
4
|
+
class PublishingPlatformHeaderSniffer
|
5
|
+
def initialize(app, header_name)
|
6
|
+
@app = app
|
7
|
+
@header_name = header_name
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
PublishingPlatformApi::PublishingPlatformHeaders.set_header(readable_name, env[@header_name])
|
12
|
+
@app.call(env)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def readable_name
|
18
|
+
@header_name.sub(/^HTTP_/, "").downcase.to_sym
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
require_relative "base"
|
2
|
+
require_relative "exceptions"
|
3
|
+
|
4
|
+
# Adapter for the Publishing API.
|
5
|
+
#
|
6
|
+
# @api documented
|
7
|
+
class PublishingPlatformApi::PublishingApi < PublishingPlatformApi::Base
|
8
|
+
class NoLiveVersion < PublishingPlatformApi::BaseError; end
|
9
|
+
|
10
|
+
# Put a content item
|
11
|
+
#
|
12
|
+
# @param content_id [UUID]
|
13
|
+
# @param payload [Hash] A valid content item
|
14
|
+
def put_content(content_id, payload)
|
15
|
+
put_json(content_url(content_id), payload)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Return a content item
|
19
|
+
#
|
20
|
+
# Raises exception if the item doesn't exist.
|
21
|
+
#
|
22
|
+
# @param content_id [UUID]
|
23
|
+
# @param params [Hash]
|
24
|
+
#
|
25
|
+
# @return [PublishingPlatformApi::Response] a content item
|
26
|
+
#
|
27
|
+
# @raise [HTTPNotFound] when the content item is not found
|
28
|
+
def get_content(content_id, params = {})
|
29
|
+
get_json(content_url(content_id, params))
|
30
|
+
end
|
31
|
+
|
32
|
+
# Publish a content item
|
33
|
+
#
|
34
|
+
# The publishing-api will "publish" a draft item, so that it will be visible
|
35
|
+
# on the public site.
|
36
|
+
#
|
37
|
+
# @param content_id [UUID]
|
38
|
+
# @param update_type [String] Either 'major', 'minor' or 'republish'
|
39
|
+
# @param options [Hash]
|
40
|
+
def publish(content_id, update_type = nil, options = {})
|
41
|
+
params = {
|
42
|
+
update_type:,
|
43
|
+
}
|
44
|
+
|
45
|
+
optional_keys = %i[previous_version]
|
46
|
+
|
47
|
+
params = merge_optional_keys(params, options, optional_keys)
|
48
|
+
|
49
|
+
post_json(publish_url(content_id), params)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Republish a content item
|
53
|
+
#
|
54
|
+
# The publishing-api will "republish" a live edition. This can be used to remove an unpublishing or to
|
55
|
+
# re-send a published edition downstream
|
56
|
+
#
|
57
|
+
# @param content_id [UUID]
|
58
|
+
# @param options [Hash]
|
59
|
+
def republish(content_id, options = {})
|
60
|
+
optional_keys = %i[previous_version]
|
61
|
+
|
62
|
+
params = merge_optional_keys({}, options, optional_keys)
|
63
|
+
|
64
|
+
post_json(republish_url(content_id), params)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Unpublish a content item
|
68
|
+
#
|
69
|
+
# The publishing API will "unpublish" a live item, to remove it from the public
|
70
|
+
# site, or update an existing unpublishing.
|
71
|
+
#
|
72
|
+
# @param content_id [UUID]
|
73
|
+
# @param type [String] Either 'withdrawal', 'gone' or 'redirect'.
|
74
|
+
# @param explanation [String] (optional) Text to show on the page.
|
75
|
+
# @param alternative_path [String] (optional) Alternative path to show on the page or redirect to.
|
76
|
+
# @param discard_drafts [Boolean] (optional) Whether to discard drafts on that item. Defaults to false.
|
77
|
+
# @param previous_version [Integer] (optional) A lock version number for optimistic locking.
|
78
|
+
# @param unpublished_at [Time] (optional) The time the content was withdrawn. Ignored for types other than withdrawn
|
79
|
+
# @param redirects [Array] (optional) Required if no alternative_path is given. An array of redirect values, ie: { path:, type:, destination: }
|
80
|
+
def unpublish(content_id, type:, explanation: nil, alternative_path: nil, discard_drafts: false, allow_draft: false, previous_version: nil, unpublished_at: nil, redirects: nil)
|
81
|
+
params = {
|
82
|
+
type:,
|
83
|
+
}
|
84
|
+
|
85
|
+
params[:explanation] = explanation if explanation
|
86
|
+
params[:alternative_path] = alternative_path if alternative_path
|
87
|
+
params[:previous_version] = previous_version if previous_version
|
88
|
+
params[:discard_drafts] = discard_drafts if discard_drafts
|
89
|
+
params[:allow_draft] = allow_draft if allow_draft
|
90
|
+
params[:unpublished_at] = unpublished_at.utc.iso8601 if unpublished_at
|
91
|
+
params[:redirects] = redirects if redirects
|
92
|
+
|
93
|
+
post_json(unpublish_url(content_id), params)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Discard a draft
|
97
|
+
#
|
98
|
+
# Deletes the draft content item.
|
99
|
+
#
|
100
|
+
# @param options [Hash]
|
101
|
+
# @option options [Integer] previous_version used to ensure the request is discarding the latest lock version of the draft
|
102
|
+
def discard_draft(content_id, options = {})
|
103
|
+
optional_keys = %i[previous_version]
|
104
|
+
|
105
|
+
params = merge_optional_keys({}, options, optional_keys)
|
106
|
+
|
107
|
+
post_json(discard_url(content_id), params)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Patch the links of a content item
|
111
|
+
#
|
112
|
+
# @param content_id [UUID]
|
113
|
+
# @param params [Hash]
|
114
|
+
# @option params [Hash] links A "links hash"
|
115
|
+
# @option params [Integer] previous_version The previous version (returned by `get_links`). If this version is not the current version, the publishing-api will reject the change and return 409 Conflict. (optional)
|
116
|
+
# @example
|
117
|
+
#
|
118
|
+
# publishing_api.patch_links(
|
119
|
+
# '86963c13-1f57-4005-b119-e7cf3cb92ecf',
|
120
|
+
# links: {
|
121
|
+
# topics: ['d6e1527d-d0c0-40d5-9603-b9f3e6866b8a'],
|
122
|
+
# mainstream_browse_pages: ['d6e1527d-d0c0-40d5-9603-b9f3e6866b8a'],
|
123
|
+
# },
|
124
|
+
# previous_version: 10,
|
125
|
+
# bulk_publishing: true
|
126
|
+
# )
|
127
|
+
#
|
128
|
+
def patch_links(content_id, params)
|
129
|
+
payload = {
|
130
|
+
links: params.fetch(:links),
|
131
|
+
}
|
132
|
+
|
133
|
+
payload = merge_optional_keys(payload, params, %i[previous_version bulk_publishing])
|
134
|
+
|
135
|
+
patch_json(links_url(content_id), payload)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Get a list of content items from the Publishing API.
|
139
|
+
#
|
140
|
+
# The only required key in the params hash is `document_type`. These will be used to filter down the content items being returned by the API. Other allowed options can be seen from the link below.
|
141
|
+
#
|
142
|
+
# @param params [Hash] At minimum, this hash has to include the `document_type` of the content items we wish to see. All other optional keys are documented above.
|
143
|
+
#
|
144
|
+
# @example
|
145
|
+
#
|
146
|
+
# publishing_api.get_content_items(
|
147
|
+
# document_type: 'taxon',
|
148
|
+
# q: 'Driving',
|
149
|
+
# page: 1,
|
150
|
+
# per_page: 50,
|
151
|
+
# publishing_app: 'content-tagger',
|
152
|
+
# fields: ['title', 'description', 'public_updated_at'],
|
153
|
+
# order: '-public_updated_at'
|
154
|
+
# )
|
155
|
+
def get_content_items(params)
|
156
|
+
query = query_string(params)
|
157
|
+
get_json("#{endpoint}/content#{query}")
|
158
|
+
end
|
159
|
+
|
160
|
+
# Reserves a path for a publishing application
|
161
|
+
#
|
162
|
+
# Returns success or failure only.
|
163
|
+
#
|
164
|
+
# @param payload [Hash]
|
165
|
+
# @option payload [Hash] publishing_app The publishing application, like `content-tagger`
|
166
|
+
def put_path(base_path, payload)
|
167
|
+
url = "#{endpoint}/paths#{base_path}"
|
168
|
+
put_json(url, payload)
|
169
|
+
end
|
170
|
+
|
171
|
+
def unreserve_path(base_path, publishing_app)
|
172
|
+
payload = { publishing_app: }
|
173
|
+
delete_json(unreserve_url(base_path), payload)
|
174
|
+
end
|
175
|
+
|
176
|
+
private
|
177
|
+
|
178
|
+
def content_url(content_id, params = {})
|
179
|
+
validate_content_id(content_id)
|
180
|
+
query = query_string(params)
|
181
|
+
"#{endpoint}/content/#{content_id}#{query}"
|
182
|
+
end
|
183
|
+
|
184
|
+
def links_url(content_id)
|
185
|
+
validate_content_id(content_id)
|
186
|
+
"#{endpoint}/links/#{content_id}"
|
187
|
+
end
|
188
|
+
|
189
|
+
def publish_url(content_id)
|
190
|
+
validate_content_id(content_id)
|
191
|
+
"#{endpoint}/content/#{content_id}/publish"
|
192
|
+
end
|
193
|
+
|
194
|
+
def republish_url(content_id)
|
195
|
+
validate_content_id(content_id)
|
196
|
+
"#{endpoint}/content/#{content_id}/republish"
|
197
|
+
end
|
198
|
+
|
199
|
+
def unpublish_url(content_id)
|
200
|
+
validate_content_id(content_id)
|
201
|
+
"#{endpoint}/content/#{content_id}/unpublish"
|
202
|
+
end
|
203
|
+
|
204
|
+
def discard_url(content_id)
|
205
|
+
validate_content_id(content_id)
|
206
|
+
"#{endpoint}/content/#{content_id}/discard-draft"
|
207
|
+
end
|
208
|
+
|
209
|
+
def unreserve_url(base_path)
|
210
|
+
"#{endpoint}/paths#{base_path}"
|
211
|
+
end
|
212
|
+
|
213
|
+
def merge_optional_keys(params, options, optional_keys)
|
214
|
+
optional_keys.each_with_object(params) do |optional_key, hash|
|
215
|
+
hash.merge!(optional_key => options[optional_key]) if options[optional_key]
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def validate_content_id(content_id)
|
220
|
+
raise ArgumentError, "content_id cannot be nil" unless content_id
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module PublishingPlatformApi
|
2
|
+
class PublishingPlatformHeaders
|
3
|
+
class << self
|
4
|
+
def set_header(header_name, value)
|
5
|
+
header_data[header_name] = value
|
6
|
+
end
|
7
|
+
|
8
|
+
def headers
|
9
|
+
header_data.reject { |_k, v| v.nil? || v.empty? }
|
10
|
+
end
|
11
|
+
|
12
|
+
def clear_headers
|
13
|
+
Thread.current[:headers] = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def header_data
|
19
|
+
Thread.current[:headers] ||= {}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative "middleware/publishing_platform_header_sniffer"
|
2
|
+
|
3
|
+
module PublishingPlatformApi
|
4
|
+
class Railtie < Rails::Railtie
|
5
|
+
initializer "publishing_platform_api.initialize_publishing_platform_request_id_sniffer" do |app|
|
6
|
+
Rails.logger.debug "Using middleware PublishingPlatformApi::PublishingPlatformHeaderSniffer to sniff for PublishingPlatform-Request-Id header"
|
7
|
+
app.middleware.use PublishingPlatformApi::PublishingPlatformHeaderSniffer, "HTTP_PUBLISHING_PLATFORM_REQUEST_ID"
|
8
|
+
end
|
9
|
+
|
10
|
+
initializer "publishing_platform_api.initialize_publishing_platform_original_url_sniffer" do |app|
|
11
|
+
Rails.logger.debug "Using middleware PublishingPlatformApi::PublishingPlatformHeaderSniffer to sniff for PublishingPlatform-Original-Url header"
|
12
|
+
app.middleware.use PublishingPlatformApi::PublishingPlatformHeaderSniffer, "HTTP_PUBLISHING_PLATFORM_ORIGINAL_URL"
|
13
|
+
end
|
14
|
+
|
15
|
+
initializer "publishing_platform_api.initialize_publishing_platform_authenticated_user_sniffer" do |app|
|
16
|
+
Rails.logger.debug "Using middleware PublishingPlatformApi::PublishingPlatformHeaderSniffer to sniff for X-PublishingPlatform-Authenticated-User header"
|
17
|
+
app.middleware.use PublishingPlatformApi::PublishingPlatformHeaderSniffer, "HTTP_X_PUBLISHING_PLATFORM_AUTHENTICATED_USER"
|
18
|
+
end
|
19
|
+
|
20
|
+
initializer "publishing_platform_api.initialize_publishing_platform_authenticated_user_organisation_sniffer" do |app|
|
21
|
+
Rails.logger.debug "Using middleware PublishingPlatformApi::PublishingPlatformHeaderSniffer to sniff for X-PublishingPlatform-Authenticated-User-Organisation header"
|
22
|
+
app.middleware.use PublishingPlatformApi::PublishingPlatformHeaderSniffer, "HTTP_X_PUBLISHING_PLATFORM_AUTHENTICATED_USER_ORGANISATION"
|
23
|
+
end
|
24
|
+
|
25
|
+
initializer "publishing_platform_api.initialize_publishing_platform_content_id_sniffer" do |app|
|
26
|
+
Rails.logger.debug "Using middleware PublishingPlatformApi::PublishingPlatformHeaderSniffer to sniff for PublishingPlatform-Auth-Bypass-Id header"
|
27
|
+
app.middleware.use PublishingPlatformApi::PublishingPlatformHeaderSniffer, "HTTP_PUBLISHING_PLATFORM_AUTH_BYPASS_ID"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
require "json"
|
2
|
+
require "forwardable"
|
3
|
+
|
4
|
+
module PublishingPlatformApi
|
5
|
+
# This wraps an HTTP response with a JSON body.
|
6
|
+
#
|
7
|
+
# Responses can be configured to use relative URLs for `web_url` properties.
|
8
|
+
# API endpoints should return absolute URLs so that they make sense outside of the
|
9
|
+
# Publishing Platform context. However on internal systems we want to present relative URLs.
|
10
|
+
# By specifying a base URI, this will convert all matching web_urls into relative URLs
|
11
|
+
# This is useful on non-canonical frontends, such as those in staging environments.
|
12
|
+
#
|
13
|
+
# Example:
|
14
|
+
#
|
15
|
+
# r = Response.new(response, web_urls_relative_to: "https://www.publishing-platform.co.uk")
|
16
|
+
# r['results'][0]['web_url']
|
17
|
+
# => "/bank-holidays"
|
18
|
+
class Response
|
19
|
+
extend Forwardable
|
20
|
+
include Enumerable
|
21
|
+
|
22
|
+
class CacheControl < Hash
|
23
|
+
PATTERN = /([-a-z]+)(?:\s*=\s*([^,\s]+))?,?+/i
|
24
|
+
|
25
|
+
def initialize(value = nil)
|
26
|
+
super()
|
27
|
+
parse(value)
|
28
|
+
end
|
29
|
+
|
30
|
+
def public?
|
31
|
+
self["public"]
|
32
|
+
end
|
33
|
+
|
34
|
+
def private?
|
35
|
+
self["private"]
|
36
|
+
end
|
37
|
+
|
38
|
+
def no_cache?
|
39
|
+
self["no-cache"]
|
40
|
+
end
|
41
|
+
|
42
|
+
def no_store?
|
43
|
+
self["no-store"]
|
44
|
+
end
|
45
|
+
|
46
|
+
def must_revalidate?
|
47
|
+
self["must-revalidate"]
|
48
|
+
end
|
49
|
+
|
50
|
+
def proxy_revalidate?
|
51
|
+
self["proxy-revalidate"]
|
52
|
+
end
|
53
|
+
|
54
|
+
def max_age
|
55
|
+
self["max-age"].to_i if key?("max-age")
|
56
|
+
end
|
57
|
+
|
58
|
+
def reverse_max_age
|
59
|
+
self["r-maxage"].to_i if key?("r-maxage")
|
60
|
+
end
|
61
|
+
alias_method :r_maxage, :reverse_max_age
|
62
|
+
|
63
|
+
def shared_max_age
|
64
|
+
self["s-maxage"].to_i if key?("r-maxage")
|
65
|
+
end
|
66
|
+
alias_method :s_maxage, :shared_max_age
|
67
|
+
|
68
|
+
def to_s
|
69
|
+
directives = []
|
70
|
+
values = []
|
71
|
+
|
72
|
+
each do |key, value|
|
73
|
+
if value == true
|
74
|
+
directives << key
|
75
|
+
elsif value
|
76
|
+
values << "#{key}=#{value}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
(directives.sort + values.sort).join(", ")
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def parse(header)
|
86
|
+
return if header.nil? || header.empty?
|
87
|
+
|
88
|
+
header.scan(PATTERN).each do |name, value|
|
89
|
+
self[name.downcase] = value || true
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def_delegators :to_hash, :[], :"<=>", :each, :dig
|
95
|
+
|
96
|
+
def initialize(http_response, options = {})
|
97
|
+
@http_response = http_response
|
98
|
+
@web_urls_relative_to = options[:web_urls_relative_to] ? URI.parse(options[:web_urls_relative_to]) : nil
|
99
|
+
end
|
100
|
+
|
101
|
+
def raw_response_body
|
102
|
+
@http_response.body
|
103
|
+
end
|
104
|
+
|
105
|
+
def code
|
106
|
+
# Return an integer code for consistency with HTTPErrorResponse
|
107
|
+
@http_response.code
|
108
|
+
end
|
109
|
+
|
110
|
+
def headers
|
111
|
+
@http_response.headers
|
112
|
+
end
|
113
|
+
|
114
|
+
def expires_at
|
115
|
+
if headers[:date] && cache_control.max_age
|
116
|
+
response_date = Time.parse(headers[:date])
|
117
|
+
response_date + cache_control.max_age
|
118
|
+
elsif headers[:expires]
|
119
|
+
Time.parse(headers[:expires])
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def expires_in
|
124
|
+
return unless headers[:date]
|
125
|
+
|
126
|
+
age = Time.now.utc - Time.parse(headers[:date])
|
127
|
+
|
128
|
+
if cache_control.max_age
|
129
|
+
cache_control.max_age - age.to_i
|
130
|
+
elsif headers[:expires]
|
131
|
+
Time.parse(headers[:expires]).to_i - Time.now.utc.to_i
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def cache_control
|
136
|
+
@cache_control ||= CacheControl.new(headers[:cache_control])
|
137
|
+
end
|
138
|
+
|
139
|
+
def to_hash
|
140
|
+
parsed_content
|
141
|
+
end
|
142
|
+
|
143
|
+
def parsed_content
|
144
|
+
@parsed_content ||= transform_parsed(JSON.parse(@http_response.body))
|
145
|
+
end
|
146
|
+
|
147
|
+
def present?
|
148
|
+
true
|
149
|
+
end
|
150
|
+
|
151
|
+
def blank?
|
152
|
+
false
|
153
|
+
end
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
def transform_parsed(value)
|
158
|
+
return value if @web_urls_relative_to.nil?
|
159
|
+
|
160
|
+
case value
|
161
|
+
when Hash
|
162
|
+
Hash[value.map do |k, v|
|
163
|
+
# NOTE: Don't bother transforming if the value is nil
|
164
|
+
if k == "web_url" && v
|
165
|
+
# Use relative URLs to route when the web_url value is on the
|
166
|
+
# same domain as the site root. Note that we can't just use the
|
167
|
+
# `route_to` method, as this would give us technically correct
|
168
|
+
# but potentially confusing `//host/path` URLs for URLs with the
|
169
|
+
# same scheme but different hosts.
|
170
|
+
relative_url = @web_urls_relative_to.route_to(v)
|
171
|
+
[k, relative_url.host ? v : relative_url.to_s]
|
172
|
+
else
|
173
|
+
[k, transform_parsed(v)]
|
174
|
+
end
|
175
|
+
end]
|
176
|
+
when Array
|
177
|
+
value.map { |v| transform_parsed(v) }
|
178
|
+
else
|
179
|
+
value
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require "addressable"
|
2
|
+
require "publishing_platform_location"
|
3
|
+
require "time"
|
4
|
+
require "publishing_platform_api/publishing_api"
|
5
|
+
|
6
|
+
# @api documented
|
7
|
+
module PublishingPlatformApi
|
8
|
+
# Creates a PublishingPlatformApi::PublishingApi adapter
|
9
|
+
#
|
10
|
+
# This will set a bearer token if a PUBLISHING_API_BEARER_TOKEN environment
|
11
|
+
# variable is set
|
12
|
+
#
|
13
|
+
# @return [PublishingPlatformApi::PublishingApi]
|
14
|
+
def self.publishing_api(options = {})
|
15
|
+
PublishingPlatformApi::PublishingApi.new(
|
16
|
+
PublishingPlatformLocation.find("publishing-api"),
|
17
|
+
{ bearer_token: ENV["PUBLISHING_API_BEARER_TOKEN"] }.merge(options),
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
metadata
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: publishing_platform_api_adapters
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Publishing Platform
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-07-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: addressable
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: link_header
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: null_logger
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: publishing_platform_location
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rest-client
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: publishing_platform_rubocop
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: Adapters to work with Publishing Platform APIs
|
98
|
+
email:
|
99
|
+
executables: []
|
100
|
+
extensions: []
|
101
|
+
extra_rdoc_files: []
|
102
|
+
files:
|
103
|
+
- README.md
|
104
|
+
- Rakefile
|
105
|
+
- lib/publishing_platform_api.rb
|
106
|
+
- lib/publishing_platform_api/base.rb
|
107
|
+
- lib/publishing_platform_api/exceptions.rb
|
108
|
+
- lib/publishing_platform_api/json_client.rb
|
109
|
+
- lib/publishing_platform_api/list_response.rb
|
110
|
+
- lib/publishing_platform_api/middleware/publishing_platform_header_sniffer.rb
|
111
|
+
- lib/publishing_platform_api/publishing_api.rb
|
112
|
+
- lib/publishing_platform_api/publishing_platform_headers.rb
|
113
|
+
- lib/publishing_platform_api/railtie.rb
|
114
|
+
- lib/publishing_platform_api/response.rb
|
115
|
+
- lib/publishing_platform_api/version.rb
|
116
|
+
- lib/publishing_platform_api_adapters.rb
|
117
|
+
homepage:
|
118
|
+
licenses:
|
119
|
+
- MIT
|
120
|
+
metadata: {}
|
121
|
+
post_install_message:
|
122
|
+
rdoc_options: []
|
123
|
+
require_paths:
|
124
|
+
- lib
|
125
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - ">="
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '3.0'
|
130
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
131
|
+
requirements:
|
132
|
+
- - ">="
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
requirements: []
|
136
|
+
rubygems_version: 3.3.7
|
137
|
+
signing_key:
|
138
|
+
specification_version: 4
|
139
|
+
summary: Adapters to work with Publishing Platform APIs
|
140
|
+
test_files: []
|