typesafe-sdk 0.2.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/CHANGELOG.md +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +359 -0
- data/lib/typesafe/sdk/answer_parser.rb +36 -0
- data/lib/typesafe/sdk/api_error.rb +43 -0
- data/lib/typesafe/sdk/api_error_factory.rb +36 -0
- data/lib/typesafe/sdk/api_response_validation_error.rb +20 -0
- data/lib/typesafe/sdk/api_timeout_error.rb +14 -0
- data/lib/typesafe/sdk/body_decoder.rb +18 -0
- data/lib/typesafe/sdk/choice.rb +39 -0
- data/lib/typesafe/sdk/choice_answer.rb +45 -0
- data/lib/typesafe/sdk/client.rb +108 -0
- data/lib/typesafe/sdk/configuration.rb +66 -0
- data/lib/typesafe/sdk/connection_pool.rb +82 -0
- data/lib/typesafe/sdk/error_message.rb +51 -0
- data/lib/typesafe/sdk/field_reader.rb +76 -0
- data/lib/typesafe/sdk/http_request.rb +30 -0
- data/lib/typesafe/sdk/http_response.rb +24 -0
- data/lib/typesafe/sdk/invalid_response_field.rb +14 -0
- data/lib/typesafe/sdk/json_value.rb +51 -0
- data/lib/typesafe/sdk/list_models_response.rb +41 -0
- data/lib/typesafe/sdk/model_metadata.rb +39 -0
- data/lib/typesafe/sdk/models.rb +37 -0
- data/lib/typesafe/sdk/net_http_transport.rb +53 -0
- data/lib/typesafe/sdk/noul.rb +49 -0
- data/lib/typesafe/sdk/noul_answer.rb +39 -0
- data/lib/typesafe/sdk/question_set.rb +55 -0
- data/lib/typesafe/sdk/rate_limit_error.rb +14 -0
- data/lib/typesafe/sdk/request_builder.rb +58 -0
- data/lib/typesafe/sdk/request_logger.rb +68 -0
- data/lib/typesafe/sdk/requester.rb +92 -0
- data/lib/typesafe/sdk/retry_after.rb +46 -0
- data/lib/typesafe/sdk/retry_policy.rb +96 -0
- data/lib/typesafe/sdk/score.rb +40 -0
- data/lib/typesafe/sdk/score_answer.rb +53 -0
- data/lib/typesafe/sdk/stderr_logger.rb +42 -0
- data/lib/typesafe/sdk/system_one_response.rb +48 -0
- data/lib/typesafe/sdk/usage.rb +37 -0
- data/lib/typesafe/sdk/version.rb +7 -0
- data/lib/typesafe/sdk.rb +47 -0
- metadata +108 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typesafe
|
|
4
|
+
module SDK
|
|
5
|
+
class Client
|
|
6
|
+
attr_reader :retry_policy
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def open(**options)
|
|
10
|
+
client = new(**options)
|
|
11
|
+
return client unless block_given?
|
|
12
|
+
|
|
13
|
+
begin
|
|
14
|
+
yield(client)
|
|
15
|
+
ensure
|
|
16
|
+
client.close
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(
|
|
22
|
+
api_key:,
|
|
23
|
+
model: nil,
|
|
24
|
+
base_url: nil,
|
|
25
|
+
timeout: nil,
|
|
26
|
+
headers: nil,
|
|
27
|
+
user_agent: nil,
|
|
28
|
+
retry_policy: nil,
|
|
29
|
+
logger: nil,
|
|
30
|
+
transport: nil
|
|
31
|
+
)
|
|
32
|
+
@configuration = Configuration.resolve(
|
|
33
|
+
api_key: api_key,
|
|
34
|
+
base_url: base_url,
|
|
35
|
+
model: model,
|
|
36
|
+
timeout: timeout,
|
|
37
|
+
headers: headers,
|
|
38
|
+
user_agent: user_agent,
|
|
39
|
+
logger: logger
|
|
40
|
+
)
|
|
41
|
+
@retry_policy = retry_policy || RetryPolicy.new
|
|
42
|
+
@transport = transport || NetHttpTransport.new
|
|
43
|
+
@requester = Requester.new(transport: @transport, logger: @configuration.logger)
|
|
44
|
+
@models = Models.new(configuration: @configuration, requester: @requester, retry_policy: @retry_policy)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
attr_reader :models
|
|
48
|
+
|
|
49
|
+
def model
|
|
50
|
+
configuration.model
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def base_url
|
|
54
|
+
configuration.base_url
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def timeout
|
|
58
|
+
configuration.timeout
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def user_agent
|
|
62
|
+
configuration.user_agent
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def system_one(state:, questions:, model: nil, retry_policy: nil, timeout: nil, extra_headers: nil, extra_body: nil)
|
|
66
|
+
request = RequestBuilder.build(
|
|
67
|
+
configuration: configuration,
|
|
68
|
+
http_method: "POST",
|
|
69
|
+
path: SYSTEM_ONE_PATH,
|
|
70
|
+
body: system_one_body(state: state, questions: questions, model: model, extra_body: extra_body),
|
|
71
|
+
timeout: timeout,
|
|
72
|
+
headers: extra_headers
|
|
73
|
+
)
|
|
74
|
+
requester.perform(
|
|
75
|
+
request: request,
|
|
76
|
+
response_class: SystemOneResponse,
|
|
77
|
+
retry_policy: retry_policy || self.retry_policy
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def close
|
|
82
|
+
transport.close if transport.respond_to?(:close)
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def inspect
|
|
87
|
+
"#<#{self.class.name} base_url=#{base_url.inspect} model=#{model.inspect} timeout=#{timeout.inspect}>"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
attr_reader :configuration, :transport, :requester
|
|
93
|
+
|
|
94
|
+
def system_one_body(state:, questions:, model:, extra_body:)
|
|
95
|
+
raise(Error, "state must be a string, hash, or array") unless JsonValue.content?(state)
|
|
96
|
+
|
|
97
|
+
body = {
|
|
98
|
+
"state" => JsonValue.normalize(state, path: "state"),
|
|
99
|
+
"model" => model.nil? ? configuration.model : model,
|
|
100
|
+
"questions" => QuestionSet.normalize(questions)
|
|
101
|
+
}
|
|
102
|
+
return body if extra_body.nil?
|
|
103
|
+
|
|
104
|
+
body.merge(JsonValue.normalize(extra_body.to_h, path: "extra_body"))
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typesafe
|
|
4
|
+
module SDK
|
|
5
|
+
class Configuration
|
|
6
|
+
attr_reader :base_url, :model, :timeout, :headers, :user_agent, :logger
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def resolve(api_key:, base_url: nil, model: nil, timeout: nil, headers: nil, user_agent: nil, logger: nil)
|
|
10
|
+
raise(Error, "api_key is required") unless api_key.is_a?(String) && !api_key.strip.empty?
|
|
11
|
+
|
|
12
|
+
normalized_headers = (headers || {}).to_h { |name, value| [name.to_s, value.to_s] }
|
|
13
|
+
header_user_agent = normalized_headers.find { |name, _value| name.casecmp?("User-Agent") }
|
|
14
|
+
normalized_headers.delete_if { |name, _value| name.casecmp?("User-Agent") }
|
|
15
|
+
|
|
16
|
+
new(
|
|
17
|
+
api_key: api_key,
|
|
18
|
+
base_url: base_url.nil? ? DEFAULT_BASE_URL : base_url,
|
|
19
|
+
model: model.nil? ? DEFAULT_MODEL : model,
|
|
20
|
+
timeout: validate_timeout(timeout.nil? ? DEFAULT_TIMEOUT : timeout),
|
|
21
|
+
headers: normalized_headers,
|
|
22
|
+
user_agent: validate_user_agent(user_agent || (header_user_agent && header_user_agent.last) || DEFAULT_USER_AGENT),
|
|
23
|
+
logger: logger
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def validate_timeout(value)
|
|
28
|
+
return value if value.is_a?(Numeric) && value.finite? && value.positive?
|
|
29
|
+
|
|
30
|
+
raise(Error, "timeout must be a positive, finite number of seconds")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def validate_user_agent(value)
|
|
36
|
+
return value if value.is_a?(String) && !value.strip.empty?
|
|
37
|
+
|
|
38
|
+
raise(Error, "user_agent must be a nonempty string")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def initialize(api_key:, base_url:, model:, timeout:, headers:, user_agent:, logger:)
|
|
43
|
+
@api_key = api_key
|
|
44
|
+
@base_url = base_url.sub(%r{/+\z}, "")
|
|
45
|
+
@model = model
|
|
46
|
+
@timeout = timeout
|
|
47
|
+
@headers = headers.freeze
|
|
48
|
+
@user_agent = user_agent
|
|
49
|
+
@logger = logger
|
|
50
|
+
freeze
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def authorization
|
|
54
|
+
"Bearer #{api_key}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def inspect
|
|
58
|
+
"#<#{self.class.name} base_url=#{base_url.inspect} model=#{model.inspect} timeout=#{timeout.inspect}>"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
attr_reader :api_key
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typesafe
|
|
4
|
+
module SDK
|
|
5
|
+
class ConnectionPool
|
|
6
|
+
def initialize
|
|
7
|
+
@mutex = Mutex.new
|
|
8
|
+
@idle = {}
|
|
9
|
+
@pid = Process.pid
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def checkout(uri:, timeout:)
|
|
13
|
+
connection = mutex.synchronize do
|
|
14
|
+
reset_after_fork
|
|
15
|
+
idle_for(key_for(uri)).pop
|
|
16
|
+
end
|
|
17
|
+
connection = start(uri: uri, timeout: timeout) if connection.nil?
|
|
18
|
+
apply_timeout(connection: connection, timeout: timeout)
|
|
19
|
+
connection
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def checkin(uri:, connection:)
|
|
23
|
+
return discard(connection) unless connection.started?
|
|
24
|
+
|
|
25
|
+
mutex.synchronize do
|
|
26
|
+
reset_after_fork
|
|
27
|
+
idle_for(key_for(uri)).push(connection)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def discard(connection)
|
|
32
|
+
return if connection.nil?
|
|
33
|
+
|
|
34
|
+
connection.finish if connection.started?
|
|
35
|
+
rescue IOError, SystemCallError, OpenSSL::SSL::SSLError
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def shutdown
|
|
40
|
+
connections = mutex.synchronize do
|
|
41
|
+
all = idle.values.flatten
|
|
42
|
+
idle.clear
|
|
43
|
+
all
|
|
44
|
+
end
|
|
45
|
+
connections.each { |connection| discard(connection) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
attr_reader :mutex, :idle
|
|
51
|
+
|
|
52
|
+
def key_for(uri)
|
|
53
|
+
"#{uri.scheme}://#{uri.host}:#{uri.port}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def idle_for(key)
|
|
57
|
+
idle[key] ||= []
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def reset_after_fork
|
|
61
|
+
return if @pid == Process.pid
|
|
62
|
+
|
|
63
|
+
@idle = {}
|
|
64
|
+
@pid = Process.pid
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def start(uri:, timeout:)
|
|
68
|
+
connection = Net::HTTP.new(uri.host, uri.port)
|
|
69
|
+
connection.use_ssl = uri.scheme == "https"
|
|
70
|
+
apply_timeout(connection: connection, timeout: timeout)
|
|
71
|
+
connection.start
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def apply_timeout(connection:, timeout:)
|
|
75
|
+
connection.open_timeout = timeout
|
|
76
|
+
connection.read_timeout = timeout
|
|
77
|
+
connection.write_timeout = timeout
|
|
78
|
+
connection.ssl_timeout = timeout
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typesafe
|
|
4
|
+
module SDK
|
|
5
|
+
class ErrorMessage
|
|
6
|
+
class << self
|
|
7
|
+
def extract(body)
|
|
8
|
+
return presence(body) if body.is_a?(String)
|
|
9
|
+
return unless body.is_a?(Hash)
|
|
10
|
+
|
|
11
|
+
from_error(body["error"]) ||
|
|
12
|
+
string_or_nil(body["message"]) ||
|
|
13
|
+
from_detail(body["detail"])
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def from_error(error)
|
|
19
|
+
return error if error.is_a?(String)
|
|
20
|
+
return unless error.is_a?(Hash)
|
|
21
|
+
|
|
22
|
+
string_or_nil(error["message"])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def from_detail(detail)
|
|
26
|
+
return detail if detail.is_a?(String)
|
|
27
|
+
return string_or_nil(detail["message"]) if detail.is_a?(Hash)
|
|
28
|
+
return unless detail.is_a?(Array)
|
|
29
|
+
|
|
30
|
+
presence(detail.filter_map { |entry| validation_entry(entry) }.join("; "))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def validation_entry(entry)
|
|
34
|
+
return unless entry.is_a?(Hash) && entry["msg"].is_a?(String)
|
|
35
|
+
|
|
36
|
+
location = entry["loc"]
|
|
37
|
+
path = location.is_a?(Array) ? location.reject { |item| item == "body" }.join(".") : ""
|
|
38
|
+
path.empty? ? entry["msg"] : "#{path}: #{entry["msg"]}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def string_or_nil(value)
|
|
42
|
+
value.is_a?(String) ? value : nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def presence(value)
|
|
46
|
+
value.empty? ? nil : value
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typesafe
|
|
4
|
+
module SDK
|
|
5
|
+
class FieldReader
|
|
6
|
+
INTEGER_KEY = /\A-?\d+\z/
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def object(value, path:)
|
|
10
|
+
return value if value.is_a?(Hash)
|
|
11
|
+
|
|
12
|
+
raise(InvalidResponseField, path)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def fetch(hash:, key:, path:)
|
|
16
|
+
raise(InvalidResponseField, join(path: path, key: key)) unless hash.key?(key)
|
|
17
|
+
|
|
18
|
+
hash[key]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def string(hash:, key:, path:)
|
|
22
|
+
value = fetch(hash: hash, key: key, path: path)
|
|
23
|
+
return value if value.is_a?(String)
|
|
24
|
+
|
|
25
|
+
raise(InvalidResponseField, join(path: path, key: key))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def number(hash:, key:, path:)
|
|
29
|
+
value = fetch(hash: hash, key: key, path: path)
|
|
30
|
+
return value.to_f if value.is_a?(Numeric)
|
|
31
|
+
|
|
32
|
+
raise(InvalidResponseField, join(path: path, key: key))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def optional_integer(hash:, key:, path:)
|
|
36
|
+
value = hash[key]
|
|
37
|
+
return value if value.nil? || value.is_a?(Integer)
|
|
38
|
+
|
|
39
|
+
raise(InvalidResponseField, join(path: path, key: key))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def number_map(hash:, key:, path:)
|
|
43
|
+
map_path = join(path: path, key: key)
|
|
44
|
+
object(fetch(hash: hash, key: key, path: path), path: map_path).to_h do |name, value|
|
|
45
|
+
raise(InvalidResponseField, join(path: map_path, key: name)) unless value.is_a?(Numeric)
|
|
46
|
+
|
|
47
|
+
[name, value.to_f]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def content_map(hash:, key:, path:)
|
|
52
|
+
map_path = join(path: path, key: key)
|
|
53
|
+
map = object(fetch(hash: hash, key: key, path: path), path: map_path)
|
|
54
|
+
map.each do |name, value|
|
|
55
|
+
next if value.is_a?(String) || value.is_a?(Hash) || value.is_a?(Array)
|
|
56
|
+
|
|
57
|
+
raise(InvalidResponseField, join(path: map_path, key: name))
|
|
58
|
+
end
|
|
59
|
+
map
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def integer_keys(hash:, path:)
|
|
63
|
+
hash.to_h do |key, value|
|
|
64
|
+
raise(InvalidResponseField, join(path: path, key: key)) unless INTEGER_KEY.match?(key)
|
|
65
|
+
|
|
66
|
+
[Integer(key, 10), value]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def join(path:, key:)
|
|
71
|
+
path.empty? ? key.to_s : "#{path}.#{key}"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typesafe
|
|
4
|
+
module SDK
|
|
5
|
+
class HTTPRequest
|
|
6
|
+
attr_reader :http_method, :url, :headers, :body, :timeout
|
|
7
|
+
|
|
8
|
+
def initialize(http_method:, url:, headers:, body:, timeout:)
|
|
9
|
+
@http_method = http_method
|
|
10
|
+
@url = url
|
|
11
|
+
@headers = headers.freeze
|
|
12
|
+
@body = body
|
|
13
|
+
@timeout = timeout
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def with_headers(headers)
|
|
18
|
+
self.class.new(http_method: http_method, url: url, headers: headers, body: body, timeout: timeout)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def endpoint
|
|
22
|
+
uri = URI.parse(url)
|
|
23
|
+
port = uri.port == uri.default_port ? "" : ":#{uri.port}"
|
|
24
|
+
"#{http_method} #{uri.scheme}://#{uri.host}#{port}#{uri.path}"
|
|
25
|
+
rescue URI::InvalidURIError
|
|
26
|
+
http_method
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typesafe
|
|
4
|
+
module SDK
|
|
5
|
+
class HTTPResponse
|
|
6
|
+
attr_reader :status, :headers, :body
|
|
7
|
+
|
|
8
|
+
def initialize(status:, headers:, body:)
|
|
9
|
+
@status = status
|
|
10
|
+
@headers = headers.to_h { |name, value| [name.to_s.downcase, value] }.freeze
|
|
11
|
+
@body = body.to_s
|
|
12
|
+
freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def success?
|
|
16
|
+
status.between?(200, 299)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def json
|
|
20
|
+
JSON.parse(body)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typesafe
|
|
4
|
+
module SDK
|
|
5
|
+
class InvalidResponseField < StandardError
|
|
6
|
+
attr_reader :field_path
|
|
7
|
+
|
|
8
|
+
def initialize(field_path)
|
|
9
|
+
@field_path = field_path
|
|
10
|
+
super("invalid response data at #{field_path.inspect}")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typesafe
|
|
4
|
+
module SDK
|
|
5
|
+
class JsonValue
|
|
6
|
+
class << self
|
|
7
|
+
def normalize(value, path: "value")
|
|
8
|
+
case value
|
|
9
|
+
when nil, true, false, String, Integer then value
|
|
10
|
+
when Float then normalize_float(value: value, path: path)
|
|
11
|
+
when Symbol then value.to_s
|
|
12
|
+
when Hash then normalize_hash(value: value, path: path)
|
|
13
|
+
when Array then value.each_with_index.map { |item, index| normalize(item, path: "#{path}.#{index}") }
|
|
14
|
+
else normalize_object(value: value, path: path)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def content?(value)
|
|
19
|
+
value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Hash) || value.is_a?(Array)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def normalize_float(value:, path:)
|
|
25
|
+
return value if value.finite?
|
|
26
|
+
|
|
27
|
+
raise(Error, "#{path} is #{value}, which cannot be encoded as json")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def normalize_hash(value:, path:)
|
|
31
|
+
value.each_with_object({}) do |(key, item), result|
|
|
32
|
+
unless key.is_a?(String) || key.is_a?(Symbol)
|
|
33
|
+
raise(Error, "#{path} has a #{key.class} key, json object keys must be strings or symbols")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
result[key.to_s] = normalize(item, path: "#{path}.#{key}")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def normalize_object(value:, path:)
|
|
41
|
+
if value.respond_to?(:as_json)
|
|
42
|
+
converted = value.as_json
|
|
43
|
+
return normalize(converted, path: path) unless converted.equal?(value)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
raise(Error, "#{path} is a #{value.class}, which cannot be encoded as json")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typesafe
|
|
4
|
+
module SDK
|
|
5
|
+
class ListModelsResponse
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
attr_reader :models, :http_response
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def from_body(body, request_logger: nil, http_response: nil)
|
|
12
|
+
root = FieldReader.object(body, path: "")
|
|
13
|
+
entries = FieldReader.fetch(hash: root, key: "models", path: "")
|
|
14
|
+
raise(InvalidResponseField, "models") unless entries.is_a?(Array)
|
|
15
|
+
|
|
16
|
+
models = entries.each_with_index.map do |entry, index|
|
|
17
|
+
path = "models.#{index}"
|
|
18
|
+
ModelMetadata.from_hash(FieldReader.object(entry, path: path), path: path)
|
|
19
|
+
end
|
|
20
|
+
new(models: models, http_response: http_response)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(models:, http_response: nil)
|
|
25
|
+
@models = models.freeze
|
|
26
|
+
@http_response = http_response
|
|
27
|
+
freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def each(&block)
|
|
31
|
+
models.each(&block)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def request_id
|
|
35
|
+
return unless http_response
|
|
36
|
+
|
|
37
|
+
http_response.headers[REQUEST_ID_HEADER]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typesafe
|
|
4
|
+
module SDK
|
|
5
|
+
class ModelMetadata
|
|
6
|
+
attr_reader :name, :description, :release_date
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def from_hash(hash, path:)
|
|
10
|
+
new(
|
|
11
|
+
name: FieldReader.string(hash: hash, key: "name", path: path),
|
|
12
|
+
description: FieldReader.string(hash: hash, key: "description", path: path),
|
|
13
|
+
release_date: FieldReader.string(hash: hash, key: "release_date", path: path)
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(name:, description:, release_date:)
|
|
19
|
+
@name = name
|
|
20
|
+
@description = description
|
|
21
|
+
@release_date = release_date
|
|
22
|
+
freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_h
|
|
26
|
+
{ name: name, description: description, release_date: release_date }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def ==(other)
|
|
30
|
+
other.is_a?(self.class) && other.to_h == to_h
|
|
31
|
+
end
|
|
32
|
+
alias eql? ==
|
|
33
|
+
|
|
34
|
+
def hash
|
|
35
|
+
[self.class, to_h].hash
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typesafe
|
|
4
|
+
module SDK
|
|
5
|
+
class Models
|
|
6
|
+
def initialize(configuration:, requester:, retry_policy:)
|
|
7
|
+
@configuration = configuration
|
|
8
|
+
@requester = requester
|
|
9
|
+
@default_retry_policy = retry_policy
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def list(retry_policy: nil, timeout: nil, extra_headers: nil)
|
|
13
|
+
request = RequestBuilder.build(
|
|
14
|
+
configuration: configuration,
|
|
15
|
+
http_method: "GET",
|
|
16
|
+
path: MODELS_PATH,
|
|
17
|
+
body: nil,
|
|
18
|
+
timeout: timeout,
|
|
19
|
+
headers: extra_headers
|
|
20
|
+
)
|
|
21
|
+
requester.perform(
|
|
22
|
+
request: request,
|
|
23
|
+
response_class: ListModelsResponse,
|
|
24
|
+
retry_policy: retry_policy || default_retry_policy
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def inspect
|
|
29
|
+
"#<#{self.class.name}>"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
attr_reader :configuration, :requester, :default_retry_policy
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typesafe
|
|
4
|
+
module SDK
|
|
5
|
+
class NetHttpTransport
|
|
6
|
+
REQUEST_CLASSES = { "GET" => Net::HTTP::Get, "POST" => Net::HTTP::Post }.freeze
|
|
7
|
+
CONNECTION_ERRORS = [
|
|
8
|
+
SocketError,
|
|
9
|
+
SystemCallError,
|
|
10
|
+
IOError,
|
|
11
|
+
OpenSSL::SSL::SSLError,
|
|
12
|
+
Net::HTTPBadResponse,
|
|
13
|
+
Net::ProtocolError,
|
|
14
|
+
Zlib::Error
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
def initialize(pool: ConnectionPool.new)
|
|
18
|
+
@pool = pool
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(request)
|
|
22
|
+
uri = URI.parse(request.url)
|
|
23
|
+
connection = nil
|
|
24
|
+
connection = pool.checkout(uri: uri, timeout: request.timeout)
|
|
25
|
+
raw = connection.request(build(uri: uri, request: request))
|
|
26
|
+
pool.checkin(uri: uri, connection: connection)
|
|
27
|
+
HTTPResponse.new(status: raw.code.to_i, headers: raw.each_header.to_h, body: raw.body)
|
|
28
|
+
rescue Timeout::Error
|
|
29
|
+
pool.discard(connection)
|
|
30
|
+
raise(APITimeoutError.new(timeout: request.timeout))
|
|
31
|
+
rescue *CONNECTION_ERRORS => e
|
|
32
|
+
pool.discard(connection)
|
|
33
|
+
raise(APIConnectionError, "connection error: #{e.class}: #{e.message}")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def close
|
|
37
|
+
pool.shutdown
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
attr_reader :pool
|
|
43
|
+
|
|
44
|
+
def build(uri:, request:)
|
|
45
|
+
request_class = REQUEST_CLASSES.fetch(request.http_method)
|
|
46
|
+
net_request = request_class.new(uri.request_uri)
|
|
47
|
+
request.headers.each { |name, value| net_request[name] = value }
|
|
48
|
+
net_request.body = request.body unless request.body.nil?
|
|
49
|
+
net_request
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|