celitech 2.0.7
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 +225 -0
- data/lib/celitech/client.rb +81 -0
- data/lib/celitech/config.rb +48 -0
- data/lib/celitech/environment.rb +7 -0
- data/lib/celitech/error.rb +50 -0
- data/lib/celitech/http/connection.rb +321 -0
- data/lib/celitech/http/hooks.rb +17 -0
- data/lib/celitech/http/oauth_token_manager.rb +76 -0
- data/lib/celitech/http/response.rb +35 -0
- data/lib/celitech/models/bad_request.rb +47 -0
- data/lib/celitech/models/create_purchase_ok_response.rb +63 -0
- data/lib/celitech/models/create_purchase_ok_response_profile.rb +75 -0
- data/lib/celitech/models/create_purchase_ok_response_purchase.rb +111 -0
- data/lib/celitech/models/create_purchase_request.rb +155 -0
- data/lib/celitech/models/create_purchase_request_language.rb +14 -0
- data/lib/celitech/models/create_purchase_v2_ok_response.rb +63 -0
- data/lib/celitech/models/create_purchase_v2_ok_response_profile.rb +96 -0
- data/lib/celitech/models/create_purchase_v2_ok_response_purchase.rb +70 -0
- data/lib/celitech/models/create_purchase_v2_request.rb +156 -0
- data/lib/celitech/models/create_purchase_v2_request_language.rb +14 -0
- data/lib/celitech/models/destinations.rb +80 -0
- data/lib/celitech/models/device.rb +80 -0
- data/lib/celitech/models/edit_purchase_ok_response.rb +90 -0
- data/lib/celitech/models/edit_purchase_request.rb +90 -0
- data/lib/celitech/models/get_esim_device_ok_response.rb +52 -0
- data/lib/celitech/models/get_esim_history_ok_response.rb +52 -0
- data/lib/celitech/models/get_esim_history_ok_response_esim.rb +61 -0
- data/lib/celitech/models/get_esim_ok_response.rb +52 -0
- data/lib/celitech/models/get_esim_ok_response_esim.rb +116 -0
- data/lib/celitech/models/get_purchase_consumption_ok_response.rb +70 -0
- data/lib/celitech/models/grant_type.rb +10 -0
- data/lib/celitech/models/history.rb +70 -0
- data/lib/celitech/models/list_destinations_ok_response.rb +51 -0
- data/lib/celitech/models/list_packages_ok_response.rb +64 -0
- data/lib/celitech/models/list_purchases_ok_response.rb +64 -0
- data/lib/celitech/models/o_auth_token_request.rb +84 -0
- data/lib/celitech/models/o_auth_token_response.rb +60 -0
- data/lib/celitech/models/open_model.rb +30 -0
- data/lib/celitech/models/package.rb +111 -0
- data/lib/celitech/models/packages.rb +121 -0
- data/lib/celitech/models/purchases.rb +178 -0
- data/lib/celitech/models/purchases_esim.rb +51 -0
- data/lib/celitech/models/serializable.rb +63 -0
- data/lib/celitech/models/token_ok_response.rb +47 -0
- data/lib/celitech/models/top_up_esim_ok_response.rb +63 -0
- data/lib/celitech/models/top_up_esim_ok_response_profile.rb +51 -0
- data/lib/celitech/models/top_up_esim_ok_response_purchase.rb +111 -0
- data/lib/celitech/models/top_up_esim_request.rb +145 -0
- data/lib/celitech/models/unauthorized.rb +47 -0
- data/lib/celitech/models/union_type.rb +12 -0
- data/lib/celitech/serializers/form_serializer.rb +41 -0
- data/lib/celitech/serializers/json_serializer.rb +41 -0
- data/lib/celitech/services/destinations.rb +59 -0
- data/lib/celitech/services/e_sim.rb +108 -0
- data/lib/celitech/services/i_frame.rb +59 -0
- data/lib/celitech/services/o_auth.rb +49 -0
- data/lib/celitech/services/packages.rb +82 -0
- data/lib/celitech/services/purchases.rb +195 -0
- data/lib/celitech/validator.rb +42 -0
- data/lib/celitech/version.rb +5 -0
- data/lib/celitech.rb +64 -0
- metadata +162 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'time'
|
|
6
|
+
require 'uri'
|
|
7
|
+
|
|
8
|
+
module Celitech
|
|
9
|
+
module HTTP
|
|
10
|
+
class Connection
|
|
11
|
+
attr_accessor :default_headers
|
|
12
|
+
attr_reader :base_url, :timeout, :retry_config
|
|
13
|
+
|
|
14
|
+
def base_url=(new_url)
|
|
15
|
+
new_uri = URI.parse(new_url.chomp('/'))
|
|
16
|
+
@mutex.synchronize do
|
|
17
|
+
# A caller-supplied client owns its own connection target; never tear it down
|
|
18
|
+
# or repoint it. Warn on a host/port change so requests aren't silently sent
|
|
19
|
+
# to the injected client's original host with the new path.
|
|
20
|
+
if @custom_http
|
|
21
|
+
if new_uri.host != @http.address || new_uri.port != @http.port
|
|
22
|
+
warn '[Celitech] base_url host/port change ignored: a custom ' \
|
|
23
|
+
'http_client is in use and keeps its own connection target.'
|
|
24
|
+
end
|
|
25
|
+
@base_url = new_url
|
|
26
|
+
@uri = new_uri
|
|
27
|
+
next
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
@base_url = new_url
|
|
31
|
+
@uri = new_uri
|
|
32
|
+
begin
|
|
33
|
+
@http&.finish
|
|
34
|
+
rescue StandardError
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
@http = Net::HTTP.new(@uri.host, @uri.port)
|
|
38
|
+
@http.use_ssl = @uri.scheme == 'https'
|
|
39
|
+
if @timeout
|
|
40
|
+
@http.open_timeout = @timeout
|
|
41
|
+
@http.read_timeout = @timeout
|
|
42
|
+
end
|
|
43
|
+
# Don't call @http.start here — the execute/reconnect path starts it
|
|
44
|
+
# lazily on the first request, which avoids eager DNS lookups when
|
|
45
|
+
# switching environments before a request is actually made.
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def initialize(base_url, default_headers = {}, timeout: nil, refresh_manager: nil, retry_config: nil, http_client: nil)
|
|
50
|
+
@base_url = base_url
|
|
51
|
+
@timeout = timeout
|
|
52
|
+
@retry_config = retry_config
|
|
53
|
+
@uri = URI.parse(base_url.chomp('/'))
|
|
54
|
+
# A caller-supplied http_client is used as-is: the SDK never reconfigures its
|
|
55
|
+
# transport (TLS, timeouts, proxy) — the caller owns those settings.
|
|
56
|
+
@custom_http = !http_client.nil?
|
|
57
|
+
@http = http_client || Net::HTTP.new(@uri.host, @uri.port)
|
|
58
|
+
@default_headers = { 'User-Agent' => 'postman-codegen/2.6.0 celitech/2.0.7 (ruby)' }.merge(default_headers)
|
|
59
|
+
@refresh_manager = refresh_manager
|
|
60
|
+
@mutex = Mutex.new
|
|
61
|
+
@hook = Hooks.new
|
|
62
|
+
unless @custom_http
|
|
63
|
+
@http.use_ssl = @uri.scheme == 'https'
|
|
64
|
+
if timeout
|
|
65
|
+
@http.open_timeout = timeout
|
|
66
|
+
@http.read_timeout = timeout
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
@http.start unless @http.started?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def get(path, params = {}, headers = {})
|
|
73
|
+
execute(Net::HTTP::Get.new(build_path(path, params), normalize_headers(@default_headers.merge(headers))))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def post(path, body = nil, content_type: 'application/json', headers: {})
|
|
77
|
+
req = Net::HTTP::Post.new(base_path(path), normalize_headers(@default_headers.merge(headers)))
|
|
78
|
+
set_body(req, body, content_type)
|
|
79
|
+
execute(req)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def put(path, body = nil, content_type: 'application/json', headers: {})
|
|
83
|
+
req = Net::HTTP::Put.new(base_path(path), normalize_headers(@default_headers.merge(headers)))
|
|
84
|
+
set_body(req, body, content_type)
|
|
85
|
+
execute(req)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def patch(path, body = nil, content_type: 'application/json', headers: {})
|
|
89
|
+
req = Net::HTTP::Patch.new(base_path(path), normalize_headers(@default_headers.merge(headers)))
|
|
90
|
+
set_body(req, body, content_type)
|
|
91
|
+
execute(req)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def delete(path, headers = {})
|
|
95
|
+
execute(Net::HTTP::Delete.new(base_path(path), normalize_headers(@default_headers.merge(headers))))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def head(path, params = {}, headers = {})
|
|
99
|
+
execute(Net::HTTP::Head.new(build_path(path, params), normalize_headers(@default_headers.merge(headers))))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def options(path, params = {}, headers = {})
|
|
103
|
+
execute(Net::HTTP::Options.new(build_path(path, params), normalize_headers(@default_headers.merge(headers))))
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def stream_get(path, params = {}, headers = {})
|
|
107
|
+
stream_execute(Net::HTTP::Get.new(build_path(path, params), normalize_headers(@default_headers.merge(headers))))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def stream_post(path, body = nil, content_type: 'application/json', headers: {})
|
|
111
|
+
req = Net::HTTP::Post.new(base_path(path), normalize_headers(@default_headers.merge(headers)))
|
|
112
|
+
set_body(req, body, content_type)
|
|
113
|
+
stream_execute(req)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
# Connection is opened once in initialize and reused across requests.
|
|
119
|
+
# If the server closes the socket (idle timeout, restart, etc.) we
|
|
120
|
+
# transparently reconnect once before giving up.
|
|
121
|
+
def execute(req)
|
|
122
|
+
attempts = 0
|
|
123
|
+
params = {}
|
|
124
|
+
@hook.before_request(req, params)
|
|
125
|
+
begin
|
|
126
|
+
attempts += 1
|
|
127
|
+
raw = @mutex.synchronize do
|
|
128
|
+
@http.request(req)
|
|
129
|
+
rescue IOError, Errno::ECONNRESET, Errno::EPIPE
|
|
130
|
+
reconnect
|
|
131
|
+
@http.request(req)
|
|
132
|
+
end
|
|
133
|
+
response = Response.new(raw)
|
|
134
|
+
raise APIError.new(response.status, response.body, response.headers) unless response.success?
|
|
135
|
+
@hook.after_response(req, response, params)
|
|
136
|
+
response
|
|
137
|
+
rescue APIError => e
|
|
138
|
+
if attempts < (@retry_config&.fetch(:attempts, nil) || 3) && %w[GET POST PUT DELETE PATCH HEAD OPTIONS].include?(req.method.upcase) && (e.status >= 500 || e.status == 408 || e.status == 429)
|
|
139
|
+
sleep(retry_after_delay(e) || retry_delay(attempts))
|
|
140
|
+
retry
|
|
141
|
+
end
|
|
142
|
+
@hook.on_error(e, req, params)
|
|
143
|
+
raise
|
|
144
|
+
end
|
|
145
|
+
rescue Net::ReadTimeout, Net::OpenTimeout => e
|
|
146
|
+
raise TimeoutError, e.message
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def stream_execute(req)
|
|
150
|
+
Enumerator.new do |yielder|
|
|
151
|
+
opts = { use_ssl: @uri.scheme == 'https' }
|
|
152
|
+
if @timeout
|
|
153
|
+
opts[:open_timeout] = @timeout
|
|
154
|
+
opts[:read_timeout] = @timeout
|
|
155
|
+
end
|
|
156
|
+
params = {}
|
|
157
|
+
@hook.before_request(req, params)
|
|
158
|
+
begin
|
|
159
|
+
Net::HTTP.start(@uri.host, @uri.port, **opts) do |http|
|
|
160
|
+
http.request(req) do |raw|
|
|
161
|
+
status = raw.code.to_i
|
|
162
|
+
unless status >= 200 && status < 300
|
|
163
|
+
error = APIError.new(status, raw.read_body, raw.each_header.to_h)
|
|
164
|
+
@hook.on_error(error, req, params)
|
|
165
|
+
raise error
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
content_type = raw['content-type'].to_s.split(';').first&.strip || ''
|
|
169
|
+
is_sse = content_type == 'text/event-stream'
|
|
170
|
+
buffer = +''
|
|
171
|
+
|
|
172
|
+
raw.read_body do |chunk|
|
|
173
|
+
buffer << chunk
|
|
174
|
+
while (idx = buffer.index("\n"))
|
|
175
|
+
line = buffer.slice!(0, idx + 1).chomp
|
|
176
|
+
if is_sse
|
|
177
|
+
next unless line.start_with?('data:')
|
|
178
|
+
|
|
179
|
+
data = line[5..].delete_prefix(' ')
|
|
180
|
+
next if data == '[DONE]'
|
|
181
|
+
|
|
182
|
+
yielder << JSON.parse(data)
|
|
183
|
+
else
|
|
184
|
+
yielder << JSON.parse(line) unless line.empty?
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
unless buffer.empty?
|
|
190
|
+
if is_sse
|
|
191
|
+
if buffer.start_with?('data:')
|
|
192
|
+
data = buffer[5..].delete_prefix(' ')
|
|
193
|
+
yielder << JSON.parse(data) unless data == '[DONE]'
|
|
194
|
+
end
|
|
195
|
+
else
|
|
196
|
+
yielder << JSON.parse(buffer)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
rescue Net::ReadTimeout, Net::OpenTimeout => e
|
|
202
|
+
raise TimeoutError, e.message
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Returns seconds to wait before the next retry attempt.
|
|
208
|
+
# Uses exponential backoff capped at maxDelay, plus random jitter.
|
|
209
|
+
def retry_delay(attempt)
|
|
210
|
+
base_ms = 150 * (2**(attempt - 1))
|
|
211
|
+
capped_ms = [base_ms, 5000].min
|
|
212
|
+
jitter_ms = rand(0..50)
|
|
213
|
+
(capped_ms + jitter_ms) / 1000.0
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Honors a server-directed retry delay from rate-limit response headers:
|
|
217
|
+
# Retry-After (delta-seconds or HTTP-date), or X-RateLimit-Reset (epoch seconds)
|
|
218
|
+
# when Retry-After is absent. Returns the delay in seconds clamped to
|
|
219
|
+
# [0, maxRetryAfterDelay], or nil when no usable header is present so the caller
|
|
220
|
+
# falls back to the computed exponential backoff.
|
|
221
|
+
def retry_after_delay(error)
|
|
222
|
+
headers = error.respond_to?(:headers) ? error.headers : nil
|
|
223
|
+
return nil unless headers.is_a?(Hash)
|
|
224
|
+
|
|
225
|
+
max_s = 60_000 / 1000.0
|
|
226
|
+
return nil unless max_s.positive?
|
|
227
|
+
|
|
228
|
+
# Normalize keys to lowercase hyphenated strings so lookups are robust to
|
|
229
|
+
# header casing and symbol keys regardless of how the error was constructed.
|
|
230
|
+
normalized = headers.each_with_object({}) do |(k, v), acc|
|
|
231
|
+
acc[k.to_s.downcase.tr('_', '-')] = v
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# retry-after-ms (milliseconds) is a non-standard but finer-grained hint some
|
|
235
|
+
# APIs send (e.g. OpenAI); it takes precedence over the whole-second Retry-After.
|
|
236
|
+
ms = normalized['retry-after-ms'].to_s.strip
|
|
237
|
+
return (ms.to_f / 1000.0).clamp(0.0, max_s) if ms.match?(/\A\d+(\.\d+)?\z/)
|
|
238
|
+
|
|
239
|
+
seconds = parse_retry_after(normalized['retry-after'])
|
|
240
|
+
return seconds.clamp(0.0, max_s) unless seconds.nil?
|
|
241
|
+
|
|
242
|
+
# X-RateLimit-Reset is interpreted as epoch seconds (the common convention).
|
|
243
|
+
reset = normalized['x-ratelimit-reset']
|
|
244
|
+
unless reset.nil? || reset.to_s.strip.empty?
|
|
245
|
+
seconds = reset.to_f - Time.now.to_f
|
|
246
|
+
return seconds.clamp(0.0, max_s) if seconds.positive?
|
|
247
|
+
end
|
|
248
|
+
nil
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Parses a Retry-After header value: an integer/float number of seconds, or an
|
|
252
|
+
# HTTP-date. Returns the delay in seconds (Float) or nil if unparseable.
|
|
253
|
+
def parse_retry_after(value)
|
|
254
|
+
return nil if value.nil?
|
|
255
|
+
|
|
256
|
+
str = value.to_s.strip
|
|
257
|
+
return nil if str.empty?
|
|
258
|
+
return str.to_f if str.match?(/\A\d+(\.\d+)?\z/)
|
|
259
|
+
|
|
260
|
+
begin
|
|
261
|
+
Time.httpdate(str).to_f - Time.now.to_f
|
|
262
|
+
rescue ArgumentError
|
|
263
|
+
nil
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Collapses array header values into a comma-separated string.
|
|
268
|
+
# Matches OpenAPI style=simple, explode=false (the default for header parameters).
|
|
269
|
+
def normalize_headers(hdrs)
|
|
270
|
+
return hdrs unless hdrs.any? { |_, v| v.is_a?(Array) }
|
|
271
|
+
|
|
272
|
+
hdrs.transform_values { |v| v.is_a?(Array) ? v.join(',') : v }
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def reconnect
|
|
276
|
+
begin
|
|
277
|
+
@http.finish
|
|
278
|
+
rescue StandardError
|
|
279
|
+
nil
|
|
280
|
+
end
|
|
281
|
+
@http.start
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def set_body(req, body, content_type)
|
|
285
|
+
return unless body
|
|
286
|
+
|
|
287
|
+
case content_type
|
|
288
|
+
when /json/
|
|
289
|
+
req.body = Serializers::Json.serialize(body).to_json
|
|
290
|
+
req['Content-Type'] = content_type
|
|
291
|
+
when 'application/xml'
|
|
292
|
+
req.body = Serializers::Xml.serialize(body)
|
|
293
|
+
req['Content-Type'] = 'application/xml'
|
|
294
|
+
when 'application/x-www-form-urlencoded'
|
|
295
|
+
req.body = Serializers::Form.to_urlencoded(body)
|
|
296
|
+
req['Content-Type'] = 'application/x-www-form-urlencoded'
|
|
297
|
+
when 'multipart/form-data'
|
|
298
|
+
Serializers::Form.set_multipart(req, body)
|
|
299
|
+
when %r{^text/}
|
|
300
|
+
req.body = body.to_s
|
|
301
|
+
req['Content-Type'] = content_type
|
|
302
|
+
else
|
|
303
|
+
# Binary and other raw types
|
|
304
|
+
req.body = body.respond_to?(:read) ? body.read : body.to_s
|
|
305
|
+
req['Content-Type'] = content_type
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def base_path(path)
|
|
310
|
+
"#{@uri.path.chomp('/')}#{path}"
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def build_path(path, params)
|
|
314
|
+
return base_path(path) if params.nil? || params.empty?
|
|
315
|
+
|
|
316
|
+
query = params.is_a?(Hash) ? URI.encode_www_form(params) : params
|
|
317
|
+
"#{base_path(path)}?#{query}"
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Celitech
|
|
4
|
+
module HTTP
|
|
5
|
+
# Lifecycle hooks for intercepting HTTP requests and responses.
|
|
6
|
+
# Override these methods in a subclass to add custom behaviour.
|
|
7
|
+
# Hook implementations should not raise exceptions; uncaught errors
|
|
8
|
+
# will propagate out of the connection and bypass subsequent hooks.
|
|
9
|
+
class Hooks
|
|
10
|
+
def before_request(request, params = {}); end
|
|
11
|
+
|
|
12
|
+
def after_response(request, response, params = {}); end
|
|
13
|
+
|
|
14
|
+
def on_error(error, request, params = {}); end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
module Celitech
|
|
8
|
+
module HTTP
|
|
9
|
+
class OAuthError < StandardError; end
|
|
10
|
+
|
|
11
|
+
# Manages OAuth 2.0 client credentials tokens: fetches, caches, and refreshes them.
|
|
12
|
+
# Token refresh is triggered when the token is absent or within REFRESH_BUFFER_SECS
|
|
13
|
+
# of expiry. A Mutex ensures thread-safe access to the cached token.
|
|
14
|
+
# Note: token refresh blocks all threads calling `headers` until the HTTP request
|
|
15
|
+
# completes. This is intentional — only one fetch occurs and others reuse the result.
|
|
16
|
+
class OAuthTokenManager
|
|
17
|
+
REFRESH_BUFFER_SECS = 5
|
|
18
|
+
|
|
19
|
+
def initialize(client_id:, client_secret:, token_url:)
|
|
20
|
+
@client_id = client_id
|
|
21
|
+
@client_secret = client_secret
|
|
22
|
+
@token_url = token_url
|
|
23
|
+
@access_token = nil
|
|
24
|
+
@expires_at = nil
|
|
25
|
+
@mutex = Mutex.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
attr_writer :client_id, :client_secret
|
|
29
|
+
|
|
30
|
+
def headers
|
|
31
|
+
{ 'Authorization' => "Bearer #{token}" }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Force the next call to re-fetch a fresh token.
|
|
35
|
+
def invalidate
|
|
36
|
+
@mutex.synchronize { @access_token = nil }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def token
|
|
42
|
+
@mutex.synchronize do
|
|
43
|
+
fetch_token if token_expired?
|
|
44
|
+
@access_token
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def token_expired?
|
|
49
|
+
@access_token.nil? || (@expires_at && Time.now + REFRESH_BUFFER_SECS >= @expires_at)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def fetch_token
|
|
53
|
+
uri = URI.parse(@token_url)
|
|
54
|
+
req = Net::HTTP::Post.new(uri.request_uri)
|
|
55
|
+
req.set_form_data(
|
|
56
|
+
grant_type: 'client_credentials',
|
|
57
|
+
client_id: @client_id,
|
|
58
|
+
client_secret: @client_secret,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Net::HTTP.start with a block opens and closes the connection automatically.
|
|
62
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
63
|
+
http.request(req)
|
|
64
|
+
end
|
|
65
|
+
raise OAuthError, "token request failed (HTTP #{response.code}): #{response.body}" unless response.is_a?(Net::HTTPSuccess)
|
|
66
|
+
|
|
67
|
+
body = JSON.parse(response.body)
|
|
68
|
+
raise OAuthError, 'access_token missing from token response' unless body.key?('access_token')
|
|
69
|
+
|
|
70
|
+
@access_token = body['access_token']
|
|
71
|
+
expires_in = body['expires_in']
|
|
72
|
+
@expires_at = expires_in ? Time.now + expires_in.to_i : nil
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Celitech
|
|
4
|
+
module HTTP
|
|
5
|
+
class Response
|
|
6
|
+
attr_reader :status, :headers, :body, :raw_body
|
|
7
|
+
|
|
8
|
+
def initialize(raw)
|
|
9
|
+
@status = raw.code.to_i
|
|
10
|
+
@headers = raw.each_header.to_h
|
|
11
|
+
@raw_body = raw.body
|
|
12
|
+
@body = parse_body(raw.body, @headers['content-type'])
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def success?
|
|
16
|
+
status >= 200 && status < 300
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def parse_body(raw_body, content_type)
|
|
22
|
+
return nil if raw_body.nil? || raw_body.empty?
|
|
23
|
+
|
|
24
|
+
ct = content_type.to_s.split(';').first&.strip || ''
|
|
25
|
+
return JSON.parse(raw_body) if ct.include?('json')
|
|
26
|
+
return raw_body if ct.start_with?('text/') || ct == 'application/xml'
|
|
27
|
+
|
|
28
|
+
# For binary and unknown types try JSON first, fall back to raw
|
|
29
|
+
JSON.parse(raw_body)
|
|
30
|
+
rescue JSON::ParserError
|
|
31
|
+
raw_body
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Celitech
|
|
4
|
+
module Models
|
|
5
|
+
class BadRequest
|
|
6
|
+
include ::Celitech::Models::Serializable
|
|
7
|
+
include ::Celitech::Models::OpenModel
|
|
8
|
+
|
|
9
|
+
def self.wire_keys
|
|
10
|
+
{
|
|
11
|
+
message: 'message',
|
|
12
|
+
}.freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def message
|
|
16
|
+
val = @message
|
|
17
|
+
val.equal?(::Celitech::Models::UNSET) ? nil : val
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
attr_reader :additional_properties
|
|
21
|
+
|
|
22
|
+
def initialize(message: ::Celitech::Models::UNSET, additional_properties: {})
|
|
23
|
+
@message = message
|
|
24
|
+
@additional_properties = additional_properties
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.from_hash(hash)
|
|
28
|
+
return unless hash
|
|
29
|
+
|
|
30
|
+
new(
|
|
31
|
+
message: hash.fetch('message', ::Celitech::Models::UNSET),
|
|
32
|
+
additional_properties: hash.except('message'),
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def attributes
|
|
37
|
+
{
|
|
38
|
+
message: @message,
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def open_model_extras
|
|
43
|
+
@additional_properties
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Celitech
|
|
4
|
+
module Models
|
|
5
|
+
class CreatePurchaseOkResponse
|
|
6
|
+
include ::Celitech::Models::Serializable
|
|
7
|
+
include ::Celitech::Models::OpenModel
|
|
8
|
+
|
|
9
|
+
def self.wire_keys
|
|
10
|
+
{
|
|
11
|
+
purchase: 'purchase',
|
|
12
|
+
profile: 'profile',
|
|
13
|
+
}.freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def purchase
|
|
17
|
+
val = @purchase
|
|
18
|
+
val.equal?(::Celitech::Models::UNSET) ? nil : val
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def profile
|
|
22
|
+
val = @profile
|
|
23
|
+
val.equal?(::Celitech::Models::UNSET) ? nil : val
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
attr_reader :additional_properties
|
|
27
|
+
|
|
28
|
+
def initialize(purchase: ::Celitech::Models::UNSET, profile: ::Celitech::Models::UNSET, additional_properties: {})
|
|
29
|
+
@purchase = purchase
|
|
30
|
+
@profile = profile
|
|
31
|
+
@additional_properties = additional_properties
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.from_hash(hash)
|
|
35
|
+
return unless hash
|
|
36
|
+
|
|
37
|
+
new(
|
|
38
|
+
purchase:
|
|
39
|
+
hash.key?('purchase') ? ::Celitech::Models::CreatePurchaseOkResponsePurchase.from_hash(hash['purchase']) : ::Celitech::Models::UNSET,
|
|
40
|
+
profile:
|
|
41
|
+
hash.key?('profile') ? ::Celitech::Models::CreatePurchaseOkResponseProfile.from_hash(hash['profile']) : ::Celitech::Models::UNSET,
|
|
42
|
+
additional_properties: hash.except('purchase', 'profile'),
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def attributes
|
|
47
|
+
{
|
|
48
|
+
purchase: @purchase,
|
|
49
|
+
profile: @profile,
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def validate!
|
|
54
|
+
@purchase&.validate! if !@purchase.equal?(::Celitech::Models::UNSET) && @purchase.respond_to?(:validate!)
|
|
55
|
+
@profile&.validate! if !@profile.equal?(::Celitech::Models::UNSET) && @profile.respond_to?(:validate!)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def open_model_extras
|
|
59
|
+
@additional_properties
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Celitech
|
|
4
|
+
module Models
|
|
5
|
+
class CreatePurchaseOkResponseProfile
|
|
6
|
+
include ::Celitech::Models::Serializable
|
|
7
|
+
include ::Celitech::Models::OpenModel
|
|
8
|
+
|
|
9
|
+
def self.wire_keys
|
|
10
|
+
{
|
|
11
|
+
iccid: 'iccid',
|
|
12
|
+
activation_code: 'activationCode',
|
|
13
|
+
manual_activation_code: 'manualActivationCode',
|
|
14
|
+
}.freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def iccid
|
|
18
|
+
val = @iccid
|
|
19
|
+
val.equal?(::Celitech::Models::UNSET) ? nil : val
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def activation_code
|
|
23
|
+
val = @activation_code
|
|
24
|
+
val.equal?(::Celitech::Models::UNSET) ? nil : val
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def manual_activation_code
|
|
28
|
+
val = @manual_activation_code
|
|
29
|
+
val.equal?(::Celitech::Models::UNSET) ? nil : val
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
attr_reader :additional_properties
|
|
33
|
+
|
|
34
|
+
def initialize(
|
|
35
|
+
iccid: ::Celitech::Models::UNSET,
|
|
36
|
+
activation_code: ::Celitech::Models::UNSET,
|
|
37
|
+
manual_activation_code: ::Celitech::Models::UNSET,
|
|
38
|
+
additional_properties: {}
|
|
39
|
+
)
|
|
40
|
+
@iccid = iccid
|
|
41
|
+
@activation_code = activation_code
|
|
42
|
+
@manual_activation_code = manual_activation_code
|
|
43
|
+
@additional_properties = additional_properties
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.from_hash(hash)
|
|
47
|
+
return unless hash
|
|
48
|
+
|
|
49
|
+
new(
|
|
50
|
+
iccid: hash.fetch('iccid', ::Celitech::Models::UNSET),
|
|
51
|
+
activation_code: hash.fetch('activationCode', ::Celitech::Models::UNSET),
|
|
52
|
+
manual_activation_code: hash.fetch('manualActivationCode', ::Celitech::Models::UNSET),
|
|
53
|
+
additional_properties: hash.except('iccid', 'activationCode', 'manualActivationCode'),
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def attributes
|
|
58
|
+
{
|
|
59
|
+
iccid: @iccid,
|
|
60
|
+
activation_code: @activation_code,
|
|
61
|
+
manual_activation_code: @manual_activation_code,
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def validate!
|
|
66
|
+
Validator.validate_string!(@iccid, 'iccid', min_length: 18, max_length: 22) unless @iccid.equal?(::Celitech::Models::UNSET)
|
|
67
|
+
Validator.validate_string!(@activation_code, 'activationCode', min_length: 1000, max_length: 8000) unless @activation_code.equal?(::Celitech::Models::UNSET)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def open_model_extras
|
|
71
|
+
@additional_properties
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|