publishing_platform_api_adapters 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.
- 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: []
|