end_point_blank 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +45 -0
- data/README.md +188 -0
- data/lib/end_point_blank/management/client.rb +228 -0
- data/lib/end_point_blank/management/configuration.rb +56 -0
- data/lib/end_point_blank/management/error.rb +134 -0
- data/lib/end_point_blank/management/error_codes.rb +115 -0
- data/lib/end_point_blank/management/idempotency_key.rb +33 -0
- data/lib/end_point_blank/management/page.rb +55 -0
- data/lib/end_point_blank/management/resources/api_packages.rb +104 -0
- data/lib/end_point_blank/management/resources/applications.rb +167 -0
- data/lib/end_point_blank/management/resources/base.rb +117 -0
- data/lib/end_point_blank/management/resources/clients.rb +152 -0
- data/lib/end_point_blank/management/retry_policy.rb +65 -0
- data/lib/end_point_blank/management/transport.rb +167 -0
- data/lib/end_point_blank/management/url_path.rb +18 -0
- data/lib/end_point_blank/management.rb +52 -0
- data/lib/end_point_blank/version.rb +1 -1
- data/lib/end_point_blank.rb +1 -0
- metadata +15 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "error_codes"
|
|
5
|
+
|
|
6
|
+
module EndPointBlank
|
|
7
|
+
# Reopened with the same superclass in end_point_blank.rb; declared here too
|
|
8
|
+
# so this file can be required on its own.
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
module Management
|
|
12
|
+
# Raised by {Client} for every refused or failed management API call.
|
|
13
|
+
#
|
|
14
|
+
# The API answers errors as
|
|
15
|
+
# <tt>{"error": {"code", "message", "details"}}</tt>. +code+ is stable and
|
|
16
|
+
# meant for programs; match on it (see {ErrorCodes}). +message+ is for
|
|
17
|
+
# people. +details+ is present only when there is something field-level to
|
|
18
|
+
# say, e.g. a +validation_failed+ error's fields.
|
|
19
|
+
#
|
|
20
|
+
# A code this SDK does not know still raises, carrying that code. An answer
|
|
21
|
+
# that is not the API's error shape (an HTML page from a proxy) raises with
|
|
22
|
+
# code +http_error+; a request that never got an answer raises with code
|
|
23
|
+
# +connection_error+ and no status.
|
|
24
|
+
class Error < EndPointBlank::Error
|
|
25
|
+
# @return [String] the API's error code, or one of {ErrorCodes::SDK}
|
|
26
|
+
attr_reader :code
|
|
27
|
+
# @return [Object, nil] field-level details, as the API sent them
|
|
28
|
+
attr_reader :details
|
|
29
|
+
# @return [Integer, nil] the HTTP status; nil for +connection_error+
|
|
30
|
+
attr_reader :status
|
|
31
|
+
# @return [Numeric, nil] the Retry-After header's seconds, when present
|
|
32
|
+
attr_reader :retry_after
|
|
33
|
+
# @return [String, nil] the Location header, e.g. on +idempotency_replay_unavailable+
|
|
34
|
+
attr_reader :location
|
|
35
|
+
# @return [String, nil] the X-Request-Id header, for support requests
|
|
36
|
+
attr_reader :request_id
|
|
37
|
+
# @return [String, nil] the HTTP method of the failed request
|
|
38
|
+
attr_reader :http_method
|
|
39
|
+
# @return [String, nil] the path of the failed request (no query, no host)
|
|
40
|
+
attr_reader :path
|
|
41
|
+
|
|
42
|
+
# rubocop:disable Metrics/ParameterLists
|
|
43
|
+
def initialize(message, code:, status: nil, details: nil, retry_after: nil, location: nil,
|
|
44
|
+
request_id: nil, http_method: nil, path: nil)
|
|
45
|
+
@code = code
|
|
46
|
+
@status = status
|
|
47
|
+
@details = details
|
|
48
|
+
@retry_after = retry_after
|
|
49
|
+
@location = location
|
|
50
|
+
@request_id = request_id
|
|
51
|
+
@http_method = http_method
|
|
52
|
+
@path = path
|
|
53
|
+
super(message)
|
|
54
|
+
end
|
|
55
|
+
# rubocop:enable Metrics/ParameterLists
|
|
56
|
+
|
|
57
|
+
# Builds the error for an answer outside 2xx. +headers+ is any Hash-like
|
|
58
|
+
# whose keys are header names in any case.
|
|
59
|
+
def self.from_response(status:, headers:, body:, http_method: nil, path: nil)
|
|
60
|
+
code, message, details = parse_body(status, body)
|
|
61
|
+
if code == ErrorCodes::IDEMPOTENCY_REPLAY_UNAVAILABLE
|
|
62
|
+
message = replay_unavailable_message(header(headers, "location"))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
new(message,
|
|
66
|
+
code: code, status: status, details: details,
|
|
67
|
+
retry_after: parse_retry_after(header(headers, "retry-after")),
|
|
68
|
+
location: header(headers, "location"), request_id: header(headers, "x-request-id"),
|
|
69
|
+
http_method: http_method, path: path)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Whether this error is +code+ (a String or Symbol).
|
|
73
|
+
def code?(code)
|
|
74
|
+
self.code == code.to_s
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def inspect
|
|
78
|
+
"#<#{self.class.name} code=#{code.inspect} status=#{status.inspect} message=#{message.inspect}>"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class << self
|
|
82
|
+
# The case-insensitive value of header +name+, or nil.
|
|
83
|
+
def header(headers, name)
|
|
84
|
+
return nil if headers.nil?
|
|
85
|
+
|
|
86
|
+
headers.each { |key, value| return value.to_s if key.to_s.casecmp?(name) }
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Seconds to wait from a Retry-After header: delta-seconds, or an
|
|
91
|
+
# HTTP-date. nil when absent or unreadable; never negative.
|
|
92
|
+
def parse_retry_after(value)
|
|
93
|
+
return nil if value.nil? || value.strip.empty?
|
|
94
|
+
return Integer(value.strip, 10) if value.strip.match?(/\A\d+\z/)
|
|
95
|
+
|
|
96
|
+
require "time"
|
|
97
|
+
[Time.httpdate(value.strip) - Time.now, 0].max
|
|
98
|
+
rescue ArgumentError
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def parse_body(status, body)
|
|
105
|
+
error = JSON.parse(body.to_s)["error"]
|
|
106
|
+
if error.is_a?(Hash) && error["code"].is_a?(String)
|
|
107
|
+
message = error["message"].is_a?(String) ? error["message"] : "The request failed (#{error["code"]})."
|
|
108
|
+
return [error["code"], message, error["details"]]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
unexpected(status, body)
|
|
112
|
+
rescue JSON::ParserError, TypeError, NoMethodError
|
|
113
|
+
unexpected(status, body)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def unexpected(status, body)
|
|
117
|
+
snippet = body.to_s.strip.gsub(/\s+/, " ")[0, 200]
|
|
118
|
+
message = "The management API answered HTTP #{status} without its JSON error shape"
|
|
119
|
+
message += snippet.empty? ? "." : ": #{snippet}"
|
|
120
|
+
[ErrorCodes::HTTP_ERROR, message, nil]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def replay_unavailable_message(location)
|
|
124
|
+
message = +"A request with this Idempotency-Key already succeeded, and its answer held a secret " \
|
|
125
|
+
"that is shown only once, so it can't be replayed and was not retried. Read or list " \
|
|
126
|
+
"the resource to see its current state"
|
|
127
|
+
message << " (#{location})" if location
|
|
128
|
+
message << ". If you need a secret you never received, rotate the credential."
|
|
129
|
+
message
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EndPointBlank
|
|
4
|
+
module Management
|
|
5
|
+
# Every +error.code+ the management API can answer, with its HTTP status, as
|
|
6
|
+
# app_portal's docs list them, plus the few codes this SDK raises itself
|
|
7
|
+
# when there is no API answer to read one from.
|
|
8
|
+
#
|
|
9
|
+
# Match on these rather than on messages, which are for people and may
|
|
10
|
+
# change. The list is not closed: an {Error} carrying a code missing here
|
|
11
|
+
# still raises with that code, so a newer API never breaks an older SDK.
|
|
12
|
+
module ErrorCodes
|
|
13
|
+
# Authentication and limits
|
|
14
|
+
MISSING_KEY = "missing_key"
|
|
15
|
+
INVALID_KEY = "invalid_key"
|
|
16
|
+
RUNTIME_CREDENTIAL_REFUSED = "runtime_credential_refused"
|
|
17
|
+
INSUFFICIENT_SCOPE = "insufficient_scope"
|
|
18
|
+
AUDIT_UNAVAILABLE = "audit_unavailable"
|
|
19
|
+
RATE_LIMITED = "rate_limited"
|
|
20
|
+
PLAN_LIMIT = "plan_limit"
|
|
21
|
+
|
|
22
|
+
# Requests
|
|
23
|
+
VALIDATION_FAILED = "validation_failed"
|
|
24
|
+
# No such resource in your organization (another organization's id
|
|
25
|
+
# answers this too), or no such /api/v1 path.
|
|
26
|
+
NOT_FOUND = "not_found"
|
|
27
|
+
INVALID_PAGINATION = "invalid_pagination"
|
|
28
|
+
INVALID_FILTER = "invalid_filter"
|
|
29
|
+
INVALID_IDEMPOTENCY_KEY = "invalid_idempotency_key"
|
|
30
|
+
IDEMPOTENCY_KEY_REUSED = "idempotency_key_reused"
|
|
31
|
+
IDEMPOTENCY_REQUEST_IN_PROGRESS = "idempotency_request_in_progress"
|
|
32
|
+
IDEMPOTENCY_REPLAY_UNAVAILABLE = "idempotency_replay_unavailable"
|
|
33
|
+
# Any request the API cannot read (e.g. a body that is not JSON).
|
|
34
|
+
BAD_REQUEST = "bad_request"
|
|
35
|
+
INTERNAL_SERVER_ERROR = "internal_server_error"
|
|
36
|
+
|
|
37
|
+
# Applications, environments and API packages
|
|
38
|
+
HAS_DEPENDENTS = "has_dependents"
|
|
39
|
+
PROTECTED = "protected"
|
|
40
|
+
INVALID_ENVIRONMENT_BASE_URLS = "invalid_environment_base_urls"
|
|
41
|
+
API_PACKAGE_ASSIGNED = "api_package_assigned"
|
|
42
|
+
INTAKE_SYNC_FAILED = "intake_sync_failed"
|
|
43
|
+
|
|
44
|
+
# Credentials
|
|
45
|
+
DELETE_REFUSED = "delete_refused"
|
|
46
|
+
INTAKE_CREDENTIAL = "intake_credential"
|
|
47
|
+
INTAKE_REJECTED = "intake_rejected"
|
|
48
|
+
INTAKE_UNAVAILABLE = "intake_unavailable"
|
|
49
|
+
|
|
50
|
+
# Clients, packages and grants
|
|
51
|
+
INVALID_CONTACTS = "invalid_contacts"
|
|
52
|
+
INVALID_PACKAGES = "invalid_packages"
|
|
53
|
+
INVALID_GRANTS = "invalid_grants"
|
|
54
|
+
INVALID_MANAGED = "invalid_managed"
|
|
55
|
+
CLIENT_NOT_ACCEPTED = "client_not_accepted"
|
|
56
|
+
CLIENT_ACCEPTED = "client_accepted"
|
|
57
|
+
CLIENT_NOT_MANAGED = "client_not_managed"
|
|
58
|
+
ALREADY_A_MEMBER = "already_a_member"
|
|
59
|
+
MANAGED_CLIENT_HAS_CREDENTIALS = "managed_client_has_credentials"
|
|
60
|
+
API_PACKAGE_NOT_FOUND = "api_package_not_found"
|
|
61
|
+
ENVIRONMENT_NOT_FOUND = "environment_not_found"
|
|
62
|
+
ALREADY_ASSIGNED = "already_assigned"
|
|
63
|
+
NOTHING_PUBLISHED_IN_ENVIRONMENT = "nothing_published_in_environment"
|
|
64
|
+
APPLICATION_NOT_FOUND = "application_not_found"
|
|
65
|
+
ENDPOINT_NOT_FOUND = "endpoint_not_found"
|
|
66
|
+
ENVIRONMENT_NOT_IN_APPLICATION = "environment_not_in_application"
|
|
67
|
+
ALREADY_GRANTED = "already_granted"
|
|
68
|
+
GRANT_REVOKED_CONCURRENTLY = "grant_revoked_concurrently"
|
|
69
|
+
|
|
70
|
+
# The warning code API package endpoint writes answer in +warnings+ (not
|
|
71
|
+
# an error): a client assignment of the package now derives no grant.
|
|
72
|
+
ASSIGNMENT_DERIVES_NOTHING = "assignment_derives_nothing"
|
|
73
|
+
|
|
74
|
+
# Raised by this SDK, never sent by the API: the request never got an
|
|
75
|
+
# answer (DNS, TLS, refused connection, timeout).
|
|
76
|
+
CONNECTION_ERROR = "connection_error"
|
|
77
|
+
# Raised by this SDK: an error answer whose body is not the API's JSON
|
|
78
|
+
# error shape, such as an HTML page from a proxy. +status+ still says
|
|
79
|
+
# what happened.
|
|
80
|
+
HTTP_ERROR = "http_error"
|
|
81
|
+
# Raised by this SDK: a success answer whose body is not JSON.
|
|
82
|
+
INVALID_RESPONSE = "invalid_response"
|
|
83
|
+
|
|
84
|
+
# The HTTP status the API answers each of its codes with.
|
|
85
|
+
STATUSES = {
|
|
86
|
+
MISSING_KEY => 401, INVALID_KEY => 401, RUNTIME_CREDENTIAL_REFUSED => 401,
|
|
87
|
+
INSUFFICIENT_SCOPE => 403, AUDIT_UNAVAILABLE => 503, RATE_LIMITED => 429, PLAN_LIMIT => 402,
|
|
88
|
+
VALIDATION_FAILED => 422, NOT_FOUND => 404, INVALID_PAGINATION => 400, INVALID_FILTER => 422,
|
|
89
|
+
INVALID_IDEMPOTENCY_KEY => 400, IDEMPOTENCY_KEY_REUSED => 422,
|
|
90
|
+
IDEMPOTENCY_REQUEST_IN_PROGRESS => 409, IDEMPOTENCY_REPLAY_UNAVAILABLE => 409,
|
|
91
|
+
BAD_REQUEST => 400, INTERNAL_SERVER_ERROR => 500,
|
|
92
|
+
HAS_DEPENDENTS => 422, PROTECTED => 422, INVALID_ENVIRONMENT_BASE_URLS => 422,
|
|
93
|
+
API_PACKAGE_ASSIGNED => 422, INTAKE_SYNC_FAILED => 422,
|
|
94
|
+
DELETE_REFUSED => 422, INTAKE_CREDENTIAL => 409, INTAKE_REJECTED => 422, INTAKE_UNAVAILABLE => 503,
|
|
95
|
+
INVALID_CONTACTS => 422, INVALID_PACKAGES => 422, INVALID_GRANTS => 422, INVALID_MANAGED => 422,
|
|
96
|
+
CLIENT_NOT_ACCEPTED => 422, CLIENT_ACCEPTED => 422, CLIENT_NOT_MANAGED => 422,
|
|
97
|
+
ALREADY_A_MEMBER => 422, MANAGED_CLIENT_HAS_CREDENTIALS => 422, API_PACKAGE_NOT_FOUND => 422,
|
|
98
|
+
ENVIRONMENT_NOT_FOUND => 422, ALREADY_ASSIGNED => 422, NOTHING_PUBLISHED_IN_ENVIRONMENT => 422,
|
|
99
|
+
APPLICATION_NOT_FOUND => 422, ENDPOINT_NOT_FOUND => 422, ENVIRONMENT_NOT_IN_APPLICATION => 422,
|
|
100
|
+
ALREADY_GRANTED => 422, GRANT_REVOKED_CONCURRENTLY => 409
|
|
101
|
+
}.freeze
|
|
102
|
+
|
|
103
|
+
# Every code the API can send, as strings.
|
|
104
|
+
ALL = STATUSES.keys.freeze
|
|
105
|
+
|
|
106
|
+
# Codes this SDK raises itself.
|
|
107
|
+
SDK = [CONNECTION_ERROR, HTTP_ERROR, INVALID_RESPONSE].freeze
|
|
108
|
+
|
|
109
|
+
# Whether +code+ is one this version of the SDK knows about.
|
|
110
|
+
def self.known?(code)
|
|
111
|
+
STATUSES.key?(code) || SDK.include?(code)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module EndPointBlank
|
|
6
|
+
module Management
|
|
7
|
+
# The Idempotency-Key a management API request carries: every POST has
|
|
8
|
+
# one, so a retry of it is replayed by the API rather than run twice.
|
|
9
|
+
module IdempotencyKey
|
|
10
|
+
MAX_LENGTH = 255
|
|
11
|
+
|
|
12
|
+
# The key to send for +method+: the caller's (stripped, as the API reads
|
|
13
|
+
# it), or a new random UUID v4 for a POST without one; nil for any other
|
|
14
|
+
# method, which must not be given one.
|
|
15
|
+
#
|
|
16
|
+
# @raise [ArgumentError] for a key on a non-POST, or an empty or
|
|
17
|
+
# too-long key
|
|
18
|
+
def self.resolve(method, key)
|
|
19
|
+
unless method == "POST"
|
|
20
|
+
raise ArgumentError, "idempotency_key applies to POST requests only" unless key.nil?
|
|
21
|
+
|
|
22
|
+
return nil
|
|
23
|
+
end
|
|
24
|
+
return SecureRandom.uuid if key.nil?
|
|
25
|
+
|
|
26
|
+
key = key.to_s.strip
|
|
27
|
+
return key if key.length.between?(1, MAX_LENGTH)
|
|
28
|
+
|
|
29
|
+
raise ArgumentError, "idempotency_key must be 1 to #{MAX_LENGTH} characters"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EndPointBlank
|
|
4
|
+
module Management
|
|
5
|
+
# One page of a management API list: the items (+data+, Hashes with String
|
|
6
|
+
# keys, as the API sent them) and the cursor for the next page, nil on the
|
|
7
|
+
# last one. Enumerable over its items.
|
|
8
|
+
#
|
|
9
|
+
# Pass +next_cursor+ as +after:+ to the same list call for the next page,
|
|
10
|
+
# or use the resource's +each+ to walk every page.
|
|
11
|
+
class Page
|
|
12
|
+
include Enumerable
|
|
13
|
+
|
|
14
|
+
# @return [Array<Hash>]
|
|
15
|
+
attr_reader :data
|
|
16
|
+
# @return [String, nil]
|
|
17
|
+
attr_reader :next_cursor
|
|
18
|
+
|
|
19
|
+
def initialize(data:, next_cursor:)
|
|
20
|
+
@data = data
|
|
21
|
+
@next_cursor = next_cursor
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Builds a page from a list answer, <tt>{"data" => [...], "next_cursor" => ...}</tt>.
|
|
25
|
+
def self.from_body(body)
|
|
26
|
+
body = {} unless body.is_a?(Hash)
|
|
27
|
+
new(data: Array(body["data"]), next_cursor: body["next_cursor"])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def each(&block)
|
|
31
|
+
return enum_for(:each) unless block
|
|
32
|
+
|
|
33
|
+
data.each(&block)
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Whether there is a page after this one.
|
|
38
|
+
def next_page?
|
|
39
|
+
!next_cursor.nil? && !next_cursor.empty?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def size
|
|
43
|
+
data.size
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def empty?
|
|
47
|
+
data.empty?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def inspect
|
|
51
|
+
"#<#{self.class.name} size=#{size} next_cursor=#{next_cursor.inspect}>"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module EndPointBlank
|
|
6
|
+
module Management
|
|
7
|
+
module Resources
|
|
8
|
+
# +/api_packages+: your organization's API packages, and what each one
|
|
9
|
+
# publishes (+/api_packages/:id/endpoints+).
|
|
10
|
+
#
|
|
11
|
+
# A package: <tt>{"id", "name", "organization_id", "inserted_at",
|
|
12
|
+
# "updated_at"}</tt>.
|
|
13
|
+
class ApiPackages < Base
|
|
14
|
+
# GET /api_packages. @return [Page]
|
|
15
|
+
def list(limit: nil, after: nil)
|
|
16
|
+
list_page(path("api_packages"), limit, after)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Every API package, across pages. @return [Enumerator, nil]
|
|
20
|
+
def each(limit: nil, &block)
|
|
21
|
+
each_item(path("api_packages"), limit, &block)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# GET /api_packages/:id. @return [Hash]
|
|
25
|
+
def get(id)
|
|
26
|
+
get_data(path("api_packages", id))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# POST /api_packages. @return [Hash] the new package
|
|
30
|
+
def create(name:, idempotency_key: nil)
|
|
31
|
+
post_data(path("api_packages"), { name: name }, idempotency_key)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# PATCH /api_packages/:id (rename). @return [Hash]
|
|
35
|
+
def update(id, name:)
|
|
36
|
+
patch_data(path("api_packages", id), { name: name })
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# DELETE /api_packages/:id. Refused with +api_package_assigned+ while
|
|
40
|
+
# a client holds it. @return [Hash] <tt>{"id", "deleted"}</tt>
|
|
41
|
+
def delete(id)
|
|
42
|
+
delete_data(path("api_packages", id))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# GET /api_packages/:id/endpoints: what the package publishes, one
|
|
46
|
+
# entry per application endpoint (or whole application) and
|
|
47
|
+
# environment. An entry: <tt>{"id", "api_package_id",
|
|
48
|
+
# "application_id", "endpoint_id", "all_endpoints", "endpoint",
|
|
49
|
+
# "environment_id", "inserted_at"}</tt>. @return [Page]
|
|
50
|
+
def list_endpoints(api_package_id, limit: nil, after: nil)
|
|
51
|
+
list_page(path("api_packages", api_package_id, "endpoints"), limit, after)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Every entry of the package, across pages. @return [Enumerator, nil]
|
|
55
|
+
def each_endpoint(api_package_id, limit: nil, &block)
|
|
56
|
+
each_item(path("api_packages", api_package_id, "endpoints"), limit, &block)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# POST /api_packages/:id/endpoints. Leave +endpoint_id+ nil to publish
|
|
60
|
+
# every endpoint of the application.
|
|
61
|
+
#
|
|
62
|
+
# Answers the whole body, not only +data+: <tt>{"data" => entry,
|
|
63
|
+
# "warnings" => [...]}</tt>. Each warning (code
|
|
64
|
+
# +assignment_derives_nothing+) names a client assignment of the
|
|
65
|
+
# package that derives no grant.
|
|
66
|
+
#
|
|
67
|
+
# @return [Hash] <tt>{"data", "warnings"}</tt>
|
|
68
|
+
def add_endpoint(api_package_id, application_id:, environment_id:, endpoint_id: nil, idempotency_key: nil)
|
|
69
|
+
body = compact(application_id: application_id, endpoint_id: endpoint_id, environment_id: environment_id)
|
|
70
|
+
@transport.request("POST", path("api_packages", api_package_id, "endpoints"),
|
|
71
|
+
body: body, idempotency_key: idempotency_key)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# DELETE /api_packages/:id/endpoints/:access_id. Answers the whole
|
|
75
|
+
# body, as {#add_endpoint} does.
|
|
76
|
+
#
|
|
77
|
+
# @return [Hash] <tt>{"data" => {"id", "deleted"}, "warnings" => [...]}</tt>
|
|
78
|
+
def remove_endpoint(api_package_id, access_id)
|
|
79
|
+
@transport.request("DELETE", path("api_packages", api_package_id, "endpoints", access_id))
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# +GET /endpoints+: the endpoints of your organization's applications,
|
|
84
|
+
# to find the ids {ApiPackages#add_endpoint} takes.
|
|
85
|
+
#
|
|
86
|
+
# An endpoint: <tt>{"id", "application_id", "path", "action", "public",
|
|
87
|
+
# "inserted_at", "updated_at"}</tt>.
|
|
88
|
+
class Endpoints < Base
|
|
89
|
+
# @param application_id [String, nil] only this application's endpoints
|
|
90
|
+
# @param version [String, nil] only endpoints deployed in an
|
|
91
|
+
# application version of this name
|
|
92
|
+
# @return [Page]
|
|
93
|
+
def list(application_id: nil, version: nil, limit: nil, after: nil)
|
|
94
|
+
list_page(path("endpoints"), limit, after, application_id: application_id, version: version)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Every matching endpoint, across pages. @return [Enumerator, nil]
|
|
98
|
+
def each(application_id: nil, version: nil, limit: nil, &block)
|
|
99
|
+
each_item(path("endpoints"), limit, { application_id: application_id, version: version }, &block)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module EndPointBlank
|
|
6
|
+
module Management
|
|
7
|
+
module Resources
|
|
8
|
+
# +/applications+, and each application's environments
|
|
9
|
+
# (+/applications/:application_id/environments+, an "application
|
|
10
|
+
# environment": the application placed in an environment at a base URL).
|
|
11
|
+
#
|
|
12
|
+
# An application: <tt>{"id", "name", "public", "organization_group_id",
|
|
13
|
+
# "synced_at", "inserted_at", "updated_at"}</tt>. An application
|
|
14
|
+
# environment: <tt>{"id", "application_id", "environment_id",
|
|
15
|
+
# "base_url", ...}</tt>.
|
|
16
|
+
class Applications < Base
|
|
17
|
+
# @return [Page]
|
|
18
|
+
def list(limit: nil, after: nil)
|
|
19
|
+
list_page(path("applications"), limit, after)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Enumerator, nil]
|
|
23
|
+
def each(limit: nil, &block)
|
|
24
|
+
each_item(path("applications"), limit, &block)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [Hash]
|
|
28
|
+
def get(id)
|
|
29
|
+
get_data(path("applications", id))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# POST /applications.
|
|
33
|
+
#
|
|
34
|
+
# @param environment_base_urls [Hash] environment id => base URL; at
|
|
35
|
+
# least one is required
|
|
36
|
+
# @return [Hash] the new application
|
|
37
|
+
def create(name:, environment_base_urls:, public: nil, organization_group_id: nil, idempotency_key: nil)
|
|
38
|
+
body = compact(name: name, environment_base_urls: environment_base_urls, public: public,
|
|
39
|
+
organization_group_id: organization_group_id)
|
|
40
|
+
post_data(path("applications"), body, idempotency_key)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# PATCH /applications/:id. Only the arguments given are sent.
|
|
44
|
+
# @return [Hash]
|
|
45
|
+
def update(id, name: nil, public: nil)
|
|
46
|
+
patch_data(path("applications", id), compact(name: name, public: public))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# DELETE /applications/:id. Refused with +has_dependents+ while grants
|
|
50
|
+
# or credentials depend on it. @return [Hash] <tt>{"id", "deleted"}</tt>
|
|
51
|
+
def delete(id)
|
|
52
|
+
delete_data(path("applications", id))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# GET /applications/:application_id/environments. @return [Page]
|
|
56
|
+
def list_environments(application_id, limit: nil, after: nil)
|
|
57
|
+
list_page(path("applications", application_id, "environments"), limit, after)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @return [Enumerator, nil]
|
|
61
|
+
def each_environment(application_id, limit: nil, &block)
|
|
62
|
+
each_item(path("applications", application_id, "environments"), limit, &block)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# POST /applications/:application_id/environments: place the
|
|
66
|
+
# application in an environment at +base_url+.
|
|
67
|
+
# @return [Hash] the application environment
|
|
68
|
+
def add_environment(application_id, environment_id:, base_url:, idempotency_key: nil)
|
|
69
|
+
post_data(path("applications", application_id, "environments"),
|
|
70
|
+
{ environment_id: environment_id, base_url: base_url }, idempotency_key)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# DELETE /applications/:application_id/environments/:id, where +id+ is
|
|
74
|
+
# the application environment's id. @return [Hash] <tt>{"id", "deleted"}</tt>
|
|
75
|
+
def remove_environment(application_id, id)
|
|
76
|
+
delete_data(path("applications", application_id, "environments", id))
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# +/environments+: deployment environments.
|
|
81
|
+
#
|
|
82
|
+
# An environment: <tt>{"id", "name", "domain", "is_default", ...}</tt>.
|
|
83
|
+
class Environments < Base
|
|
84
|
+
# @return [Page]
|
|
85
|
+
def list(limit: nil, after: nil)
|
|
86
|
+
list_page(path("environments"), limit, after)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @return [Enumerator, nil]
|
|
90
|
+
def each(limit: nil, &block)
|
|
91
|
+
each_item(path("environments"), limit, &block)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @return [Hash]
|
|
95
|
+
def get(id)
|
|
96
|
+
get_data(path("environments", id))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# POST /environments. @return [Hash] the new environment
|
|
100
|
+
def create(name:, domain:, is_default: nil, idempotency_key: nil)
|
|
101
|
+
post_data(path("environments"), compact(name: name, domain: domain, is_default: is_default),
|
|
102
|
+
idempotency_key)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# PATCH /environments/:id. Only the arguments given are sent. The
|
|
106
|
+
# production environment answers +protected+. @return [Hash]
|
|
107
|
+
def update(id, name: nil, domain: nil, is_default: nil)
|
|
108
|
+
patch_data(path("environments", id), compact(name: name, domain: domain, is_default: is_default))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# DELETE /environments/:id. @return [Hash] <tt>{"id", "deleted"}</tt>
|
|
112
|
+
def delete(id)
|
|
113
|
+
delete_data(path("environments", id))
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# +/credentials+: runtime client credentials (the client_id and
|
|
118
|
+
# client_secret an SDK authenticates with).
|
|
119
|
+
#
|
|
120
|
+
# A credential: <tt>{"id", "client_id", "secret_last_4",
|
|
121
|
+
# "application_environment_id", "application_id", "environment",
|
|
122
|
+
# "previous_secret", "expired_at", "created_at", "updated_at"}</tt>.
|
|
123
|
+
# {#create} and {#rotate} add +client_secret+, the only time the secret
|
|
124
|
+
# is ever returned: store it then. This SDK never logs it.
|
|
125
|
+
class Credentials < Base
|
|
126
|
+
# @param application_environment_id [String, nil] only this
|
|
127
|
+
# application environment's credentials
|
|
128
|
+
# @return [Page] metadata only, no secrets
|
|
129
|
+
def list(application_environment_id: nil, limit: nil, after: nil)
|
|
130
|
+
list_page(path("credentials"), limit, after, application_environment_id: application_environment_id)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# @return [Enumerator, nil]
|
|
134
|
+
def each(application_environment_id: nil, limit: nil, &block)
|
|
135
|
+
each_item(path("credentials"), limit, { application_environment_id: application_environment_id }, &block)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @return [Hash] metadata only, no secret
|
|
139
|
+
def get(id)
|
|
140
|
+
get_data(path("credentials", id))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# POST /credentials. A retry of a create that succeeded but whose
|
|
144
|
+
# answer was lost raises +idempotency_replay_unavailable+: list the
|
|
145
|
+
# application environment's credentials instead.
|
|
146
|
+
# @return [Hash] the credential, with its one-time +client_secret+
|
|
147
|
+
def create(application_environment_id:, idempotency_key: nil)
|
|
148
|
+
post_data(path("credentials"), { application_environment_id: application_environment_id }, idempotency_key)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# POST /credentials/:id/rotate: a new secret; the old one keeps
|
|
152
|
+
# working for the grace window (see +previous_secret+).
|
|
153
|
+
# @return [Hash] the credential, with its new one-time +client_secret+
|
|
154
|
+
def rotate(id, idempotency_key: nil)
|
|
155
|
+
post_data(path("credentials", id, "rotate"), nil, idempotency_key)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# DELETE /credentials/:id: revokes the credential on intake, then
|
|
159
|
+
# removes it. @return [Hash] <tt>{"id", "deleted"}</tt>
|
|
160
|
+
def delete(id)
|
|
161
|
+
delete_data(path("credentials", id))
|
|
162
|
+
end
|
|
163
|
+
alias revoke delete
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|