dexa-ai-sdk 0.3.5
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/.fernignore +5 -0
- data/.rubocop.yml +107 -0
- data/CONTRIBUTING.md +120 -0
- data/README.md +199 -0
- data/Rakefile +20 -0
- data/custom.gemspec.rb +23 -0
- data/lib/dexa-ai-sdk/client.rb +26 -0
- data/lib/dexa-ai-sdk/environment.rb +7 -0
- data/lib/dexa-ai-sdk/errors/api_error.rb +8 -0
- data/lib/dexa-ai-sdk/errors/client_error.rb +17 -0
- data/lib/dexa-ai-sdk/errors/connection_error.rb +11 -0
- data/lib/dexa-ai-sdk/errors/redirect_error.rb +8 -0
- data/lib/dexa-ai-sdk/errors/response_error.rb +63 -0
- data/lib/dexa-ai-sdk/errors/server_error.rb +11 -0
- data/lib/dexa-ai-sdk/errors/timeout_error.rb +8 -0
- data/lib/dexa-ai-sdk/internal/errors/constraint_error.rb +10 -0
- data/lib/dexa-ai-sdk/internal/errors/type_error.rb +10 -0
- data/lib/dexa-ai-sdk/internal/http/base_request.rb +81 -0
- data/lib/dexa-ai-sdk/internal/http/raw_client.rb +295 -0
- data/lib/dexa-ai-sdk/internal/iterators/cursor_item_iterator.rb +33 -0
- data/lib/dexa-ai-sdk/internal/iterators/cursor_page_iterator.rb +117 -0
- data/lib/dexa-ai-sdk/internal/iterators/item_iterator.rb +111 -0
- data/lib/dexa-ai-sdk/internal/iterators/offset_item_iterator.rb +35 -0
- data/lib/dexa-ai-sdk/internal/iterators/offset_page_iterator.rb +151 -0
- data/lib/dexa-ai-sdk/internal/json/request.rb +50 -0
- data/lib/dexa-ai-sdk/internal/json/serializable.rb +25 -0
- data/lib/dexa-ai-sdk/internal/multipart/multipart_encoder.rb +141 -0
- data/lib/dexa-ai-sdk/internal/multipart/multipart_form_data.rb +83 -0
- data/lib/dexa-ai-sdk/internal/multipart/multipart_form_data_part.rb +51 -0
- data/lib/dexa-ai-sdk/internal/multipart/multipart_request.rb +41 -0
- data/lib/dexa-ai-sdk/internal/types/array.rb +47 -0
- data/lib/dexa-ai-sdk/internal/types/boolean.rb +34 -0
- data/lib/dexa-ai-sdk/internal/types/enum.rb +56 -0
- data/lib/dexa-ai-sdk/internal/types/hash.rb +36 -0
- data/lib/dexa-ai-sdk/internal/types/model/field.rb +38 -0
- data/lib/dexa-ai-sdk/internal/types/model.rb +220 -0
- data/lib/dexa-ai-sdk/internal/types/type.rb +35 -0
- data/lib/dexa-ai-sdk/internal/types/union.rb +161 -0
- data/lib/dexa-ai-sdk/internal/types/unknown.rb +15 -0
- data/lib/dexa-ai-sdk/internal/types/utils.rb +123 -0
- data/lib/dexa-ai-sdk/tasks/client.rb +190 -0
- data/lib/dexa-ai-sdk/tasks/types/decide_request.rb +37 -0
- data/lib/dexa-ai-sdk/tasks/types/decide_request_output_language.rb +32 -0
- data/lib/dexa-ai-sdk/tasks/types/decide_request_response_format.rb +11 -0
- data/lib/dexa-ai-sdk/tasks/types/decide_request_response_format_type.rb +13 -0
- data/lib/dexa-ai-sdk/tasks/types/decide_request_target_language.rb +32 -0
- data/lib/dexa-ai-sdk/tasks/types/extract_request.rb +37 -0
- data/lib/dexa-ai-sdk/tasks/types/extract_request_output_language.rb +32 -0
- data/lib/dexa-ai-sdk/tasks/types/extract_request_response_format.rb +11 -0
- data/lib/dexa-ai-sdk/tasks/types/extract_request_response_format_type.rb +13 -0
- data/lib/dexa-ai-sdk/tasks/types/extract_request_target_language.rb +32 -0
- data/lib/dexa-ai-sdk/types/audio_input.rb +15 -0
- data/lib/dexa-ai-sdk/types/audio_input_type.rb +12 -0
- data/lib/dexa-ai-sdk/types/choice.rb +11 -0
- data/lib/dexa-ai-sdk/types/choice_finish_reason.rb +13 -0
- data/lib/dexa-ai-sdk/types/content_part.rb +21 -0
- data/lib/dexa-ai-sdk/types/content_part_language.rb +30 -0
- data/lib/dexa-ai-sdk/types/content_part_type.rb +15 -0
- data/lib/dexa-ai-sdk/types/decide_response.rb +21 -0
- data/lib/dexa-ai-sdk/types/decide_result.rb +11 -0
- data/lib/dexa-ai-sdk/types/decide_task.rb +21 -0
- data/lib/dexa-ai-sdk/types/decide_task_target_language.rb +30 -0
- data/lib/dexa-ai-sdk/types/error_response.rb +9 -0
- data/lib/dexa-ai-sdk/types/error_response_error.rb +15 -0
- data/lib/dexa-ai-sdk/types/extract_response.rb +19 -0
- data/lib/dexa-ai-sdk/types/extract_task.rb +21 -0
- data/lib/dexa-ai-sdk/types/extract_task_target_language.rb +30 -0
- data/lib/dexa-ai-sdk/types/image_input.rb +15 -0
- data/lib/dexa-ai-sdk/types/image_input_type.rb +12 -0
- data/lib/dexa-ai-sdk/types/image_url.rb +11 -0
- data/lib/dexa-ai-sdk/types/image_url_detail.rb +13 -0
- data/lib/dexa-ai-sdk/types/message.rb +13 -0
- data/lib/dexa-ai-sdk/types/message_content.rb +13 -0
- data/lib/dexa-ai-sdk/types/message_language.rb +30 -0
- data/lib/dexa-ai-sdk/types/message_role.rb +13 -0
- data/lib/dexa-ai-sdk/types/response_message.rb +13 -0
- data/lib/dexa-ai-sdk/types/response_message_language.rb +30 -0
- data/lib/dexa-ai-sdk/types/response_message_role.rb +11 -0
- data/lib/dexa-ai-sdk/types/run_tasks_request.rb +29 -0
- data/lib/dexa-ai-sdk/types/run_tasks_request_output_language.rb +30 -0
- data/lib/dexa-ai-sdk/types/run_tasks_request_response_format.rb +9 -0
- data/lib/dexa-ai-sdk/types/run_tasks_request_response_format_type.rb +11 -0
- data/lib/dexa-ai-sdk/types/run_tasks_request_target_language.rb +30 -0
- data/lib/dexa-ai-sdk/types/run_tasks_response.rb +19 -0
- data/lib/dexa-ai-sdk/types/task.rb +15 -0
- data/lib/dexa-ai-sdk/types/usage.rb +13 -0
- data/lib/dexa-ai-sdk/version.rb +5 -0
- data/lib/dexa-ai-sdk.rb +86 -0
- data/reference.md +414 -0
- metadata +139 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DexaAiSdk
|
|
4
|
+
module Internal
|
|
5
|
+
class OffsetPageIterator
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
# The raw HTTP response from the most recent page response.
|
|
9
|
+
# @return [Net::HTTPResponse, nil]
|
|
10
|
+
attr_reader :http_response
|
|
11
|
+
|
|
12
|
+
# Instantiates an OffsetPageIterator, an Enumerable class which wraps calls to an offset-based paginated API and yields pages of items from it.
|
|
13
|
+
#
|
|
14
|
+
# @param initial_page [Integer] The initial page to use when iterating, if any.
|
|
15
|
+
# @param item_field [Symbol] The field to pull the list of items to iterate over.
|
|
16
|
+
# @param has_next_field [Symbol] The field to pull the boolean of whether a next page exists from, if any.
|
|
17
|
+
# @param step [Boolean] If true, treats the page number as a true offset (i.e. increments the page number by the number of items returned from each call rather than just 1)
|
|
18
|
+
# @param block [Proc] A block which is responsible for receiving a page number to use and returning the given page from the API.
|
|
19
|
+
# The block should return a two-element array: [parsed_page, raw_http_response].
|
|
20
|
+
# @return [DexaAiSdk::Internal::OffsetPageIterator]
|
|
21
|
+
def initialize(initial_page:, item_field:, has_next_field:, step:, &block)
|
|
22
|
+
@initial_page = initial_page || (step ? 0 : 1)
|
|
23
|
+
@item_field = item_field
|
|
24
|
+
@has_next_field = has_next_field
|
|
25
|
+
@step = step
|
|
26
|
+
@get_next_page = block
|
|
27
|
+
@http_response = nil
|
|
28
|
+
@first_page_requested = false
|
|
29
|
+
@first_page = nil
|
|
30
|
+
rewind
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Sends the request for the first page now instead of on the first iteration, so an API error for that page
|
|
34
|
+
# is raised here. The page is kept, and every loop that starts from the first page reuses it instead of
|
|
35
|
+
# requesting it again (an empty first page ends those loops without a new request). Does nothing if the first
|
|
36
|
+
# page was already requested.
|
|
37
|
+
#
|
|
38
|
+
# @return [self]
|
|
39
|
+
def load_first_page
|
|
40
|
+
return self if @first_page_requested
|
|
41
|
+
|
|
42
|
+
@first_page = fetch_page(@initial_page)
|
|
43
|
+
rewind
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Uses the first page that `other` loaded with `load_first_page`, unless this iterator already sent a request.
|
|
48
|
+
#
|
|
49
|
+
# @param other [DexaAiSdk::Internal::OffsetPageIterator]
|
|
50
|
+
# @return [NilClass]
|
|
51
|
+
def reuse_first_page(other)
|
|
52
|
+
return if @first_page_requested || other.first_page.nil?
|
|
53
|
+
|
|
54
|
+
@first_page = other.first_page
|
|
55
|
+
rewind
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Iterates over each page returned by the API, starting again from the first page on every call.
|
|
59
|
+
#
|
|
60
|
+
# @param block [Proc] The block which each retrieved page is yielded to.
|
|
61
|
+
# @return [NilClass, Enumerator] An Enumerator when no block is given.
|
|
62
|
+
def each(&block)
|
|
63
|
+
return enum_for(:each) unless block_given?
|
|
64
|
+
|
|
65
|
+
rewind
|
|
66
|
+
while (page = next_page)
|
|
67
|
+
block.call(page)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Resets page-by-page iteration (`next_page` / `next?`) to the first page.
|
|
72
|
+
#
|
|
73
|
+
# @return [NilClass]
|
|
74
|
+
def rewind
|
|
75
|
+
@page_number = @initial_page
|
|
76
|
+
# A cache of whether the API has another page, if it gives us that information...
|
|
77
|
+
@has_next_page = nil
|
|
78
|
+
# ...or the actual next page, preloaded, if it doesn't. A first page kept by `load_first_page` is reused.
|
|
79
|
+
@next_page = @first_page
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Whether another page will be available from the API.
|
|
84
|
+
#
|
|
85
|
+
# @return [Boolean]
|
|
86
|
+
def next?
|
|
87
|
+
return @has_next_page unless @has_next_page.nil?
|
|
88
|
+
return true if @next_page && page_has_items?(@next_page)
|
|
89
|
+
return @has_next_page = false if @next_page
|
|
90
|
+
|
|
91
|
+
fetched_page = fetch_page(@page_number)
|
|
92
|
+
fetched_page_items = fetched_page&.send(@item_field)
|
|
93
|
+
if fetched_page_items.nil? || fetched_page_items.empty?
|
|
94
|
+
@has_next_page = false
|
|
95
|
+
else
|
|
96
|
+
@next_page = fetched_page
|
|
97
|
+
true
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Returns the next page from the API.
|
|
102
|
+
def next_page
|
|
103
|
+
return nil if @page_number.nil?
|
|
104
|
+
|
|
105
|
+
if @next_page
|
|
106
|
+
this_page = @next_page
|
|
107
|
+
@next_page = nil
|
|
108
|
+
else
|
|
109
|
+
this_page = fetch_page(@page_number)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
@has_next_page = this_page&.send(@has_next_field) if @has_next_field
|
|
113
|
+
|
|
114
|
+
items = this_page.send(@item_field)
|
|
115
|
+
if items.nil? || items.empty?
|
|
116
|
+
@page_number = nil
|
|
117
|
+
return nil
|
|
118
|
+
elsif @step
|
|
119
|
+
@page_number += items.length
|
|
120
|
+
else
|
|
121
|
+
@page_number += 1
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
this_page
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
protected
|
|
128
|
+
|
|
129
|
+
attr_reader :first_page
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def page_has_items?(page)
|
|
134
|
+
items = page.send(@item_field)
|
|
135
|
+
!items.nil? && !items.empty?
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def fetch_page(page_number)
|
|
139
|
+
result = @get_next_page.call(page_number)
|
|
140
|
+
@first_page_requested = true
|
|
141
|
+
if result.is_a?(Array)
|
|
142
|
+
fetched_page, raw_response = result
|
|
143
|
+
@http_response = raw_response
|
|
144
|
+
fetched_page
|
|
145
|
+
else
|
|
146
|
+
result
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DexaAiSdk
|
|
4
|
+
module Internal
|
|
5
|
+
module JSON
|
|
6
|
+
# @api private
|
|
7
|
+
class Request < DexaAiSdk::Internal::Http::BaseRequest
|
|
8
|
+
attr_reader :body
|
|
9
|
+
|
|
10
|
+
# @param base_url [String] The base URL for the request
|
|
11
|
+
# @param path [String] The path for the request
|
|
12
|
+
# @param method [Symbol] The HTTP method for the request (:get, :post, etc.)
|
|
13
|
+
# @param headers [Hash] Additional headers for the request (optional)
|
|
14
|
+
# @param query [Hash] Query parameters for the request (optional)
|
|
15
|
+
# @param body [Object, nil] The JSON request body (optional)
|
|
16
|
+
# @param request_options [DexaAiSdk::RequestOptions, Hash{Symbol=>Object}, nil]
|
|
17
|
+
def initialize(base_url:, path:, method:, headers: {}, query: {}, body: nil, request_options: {})
|
|
18
|
+
super(base_url:, path:, method:, headers:, query:, request_options:)
|
|
19
|
+
|
|
20
|
+
@body = body
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Hash] The encoded HTTP request headers.
|
|
24
|
+
# @param protected_keys [Array<String>] Header keys set by the SDK client (e.g. auth, metadata)
|
|
25
|
+
# that must not be overridden by additional_headers from request_options.
|
|
26
|
+
def encode_headers(protected_keys: [])
|
|
27
|
+
sdk_headers = {
|
|
28
|
+
"Content-Type" => "application/json",
|
|
29
|
+
"Accept" => "application/json"
|
|
30
|
+
}.merge(@headers)
|
|
31
|
+
merge_additional_headers(sdk_headers, protected_keys:)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @return [String, nil] The encoded HTTP request body, including any additional_body_parameters
|
|
35
|
+
# from request_options.
|
|
36
|
+
def encode_body
|
|
37
|
+
body = request_body
|
|
38
|
+
body.nil? ? nil : ::JSON.generate(body)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# @return [Object, nil] The request body merged with additional_body_parameters.
|
|
44
|
+
def request_body
|
|
45
|
+
merge_additional_body_parameters(@body)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DexaAiSdk
|
|
4
|
+
module Internal
|
|
5
|
+
module JSON
|
|
6
|
+
module Serializable
|
|
7
|
+
# Loads data from JSON into its deserialized form
|
|
8
|
+
#
|
|
9
|
+
# @param str [String] Raw JSON to load into an object
|
|
10
|
+
# @return [Object]
|
|
11
|
+
def load(str)
|
|
12
|
+
raise NotImplementedError
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Dumps data from its deserialized form into JSON
|
|
16
|
+
#
|
|
17
|
+
# @param value [Object] The deserialized value
|
|
18
|
+
# @return [String]
|
|
19
|
+
def dump(value)
|
|
20
|
+
raise NotImplementedError
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DexaAiSdk
|
|
4
|
+
module Internal
|
|
5
|
+
module Multipart
|
|
6
|
+
# Encodes parameters into a `multipart/form-data` payload as described by RFC
|
|
7
|
+
# 2388:
|
|
8
|
+
#
|
|
9
|
+
# https://tools.ietf.org/html/rfc2388
|
|
10
|
+
#
|
|
11
|
+
# This is most useful for transferring file-like objects.
|
|
12
|
+
#
|
|
13
|
+
# Parameters should be added with `#encode`. When ready, use `#body` to get
|
|
14
|
+
# the encoded result and `#content_type` to get the value that should be
|
|
15
|
+
# placed in the `Content-Type` header of a subsequent request (which includes
|
|
16
|
+
# a boundary value).
|
|
17
|
+
#
|
|
18
|
+
# This abstraction is heavily inspired by Stripe's multipart/form-data implementation,
|
|
19
|
+
# which can be found here:
|
|
20
|
+
#
|
|
21
|
+
# https://github.com/stripe/stripe-ruby/blob/ca00b676f04ac421cf5cb5ff0325f243651677b6/lib/stripe/multipart_encoder.rb#L18
|
|
22
|
+
#
|
|
23
|
+
# @api private
|
|
24
|
+
class Encoder
|
|
25
|
+
CONTENT_TYPE = "multipart/form-data"
|
|
26
|
+
CRLF = "\r\n"
|
|
27
|
+
|
|
28
|
+
attr_reader :boundary, :body
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
# Chose the same number of random bytes that Go uses in its standard
|
|
32
|
+
# library implementation. Easily enough entropy to ensure that it won't
|
|
33
|
+
# be present in a file we're sending.
|
|
34
|
+
@boundary = SecureRandom.hex(30)
|
|
35
|
+
|
|
36
|
+
@body = String.new
|
|
37
|
+
@closed = false
|
|
38
|
+
@first_field = true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Gets the content type string including the boundary.
|
|
42
|
+
#
|
|
43
|
+
# @return [String] The content type with boundary
|
|
44
|
+
def content_type
|
|
45
|
+
"#{CONTENT_TYPE}; boundary=#{@boundary}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Encode the given FormData object into a multipart/form-data payload.
|
|
49
|
+
#
|
|
50
|
+
# @param form_data [FormData] The form data to encode
|
|
51
|
+
# @return [String] The encoded body.
|
|
52
|
+
def encode(form_data)
|
|
53
|
+
return "" if form_data.parts.empty?
|
|
54
|
+
|
|
55
|
+
form_data.parts.each do |part|
|
|
56
|
+
write_part(part)
|
|
57
|
+
end
|
|
58
|
+
close
|
|
59
|
+
|
|
60
|
+
@body
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Writes a FormDataPart to the encoder.
|
|
64
|
+
#
|
|
65
|
+
# @param part [FormDataPart] The part to write
|
|
66
|
+
# @return [nil]
|
|
67
|
+
def write_part(part)
|
|
68
|
+
raise "Cannot write to closed encoder" if @closed
|
|
69
|
+
|
|
70
|
+
write_field(
|
|
71
|
+
name: part.name,
|
|
72
|
+
data: part.contents,
|
|
73
|
+
filename: part.filename,
|
|
74
|
+
headers: part.headers
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Writes a field to the encoder.
|
|
81
|
+
#
|
|
82
|
+
# @param name [String] The field name
|
|
83
|
+
# @param data [String] The field data
|
|
84
|
+
# @param filename [String, nil] Optional filename
|
|
85
|
+
# @param headers [Hash<String, String>, nil] Optional additional headers
|
|
86
|
+
# @return [nil]
|
|
87
|
+
def write_field(name:, data:, filename: nil, headers: nil)
|
|
88
|
+
raise "Cannot write to closed encoder" if @closed
|
|
89
|
+
|
|
90
|
+
if @first_field
|
|
91
|
+
@first_field = false
|
|
92
|
+
else
|
|
93
|
+
@body << CRLF
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
@body << "--#{@boundary}#{CRLF}"
|
|
97
|
+
@body << %(Content-Disposition: form-data; name="#{escape(name.to_s)}")
|
|
98
|
+
@body << %(; filename="#{escape(filename)}") if filename
|
|
99
|
+
@body << CRLF
|
|
100
|
+
|
|
101
|
+
if headers
|
|
102
|
+
headers.each do |key, value|
|
|
103
|
+
@body << "#{key}: #{value}#{CRLF}"
|
|
104
|
+
end
|
|
105
|
+
elsif filename
|
|
106
|
+
# Default content type for files.
|
|
107
|
+
@body << "Content-Type: application/octet-stream#{CRLF}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
@body << CRLF
|
|
111
|
+
@body << data.to_s
|
|
112
|
+
|
|
113
|
+
nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Finalizes the encoder by writing the final boundary.
|
|
117
|
+
#
|
|
118
|
+
# @return [nil]
|
|
119
|
+
def close
|
|
120
|
+
raise "Encoder already closed" if @closed
|
|
121
|
+
|
|
122
|
+
@body << CRLF
|
|
123
|
+
@body << "--#{@boundary}--"
|
|
124
|
+
@closed = true
|
|
125
|
+
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
# Escapes quotes for use in header values and replaces line breaks with spaces.
|
|
132
|
+
#
|
|
133
|
+
# @param str [String] The string to escape
|
|
134
|
+
# @return [String] The escaped string
|
|
135
|
+
def escape(str)
|
|
136
|
+
str.to_s.gsub('"', "%22").tr("\n", " ").tr("\r", " ")
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DexaAiSdk
|
|
4
|
+
module Internal
|
|
5
|
+
module Multipart
|
|
6
|
+
# @api private
|
|
7
|
+
class FormData
|
|
8
|
+
# @return [Array<FormDataPart>] The parts in this multipart form data.
|
|
9
|
+
attr_reader :parts
|
|
10
|
+
|
|
11
|
+
# @return [Encoder] The encoder for this multipart form data.
|
|
12
|
+
private attr_reader :encoder
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@encoder = Encoder.new
|
|
16
|
+
@parts = []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Adds a new part to the multipart form data.
|
|
20
|
+
#
|
|
21
|
+
# @param name [String] The name of the form field
|
|
22
|
+
# @param value [String, Integer, Float, Boolean, #read] The value of the field
|
|
23
|
+
# @param content_type [String, nil] Optional content type
|
|
24
|
+
# @return [self] Returns self for chaining
|
|
25
|
+
def add(name:, value:, content_type: nil)
|
|
26
|
+
headers = content_type ? { "Content-Type" => content_type } : nil
|
|
27
|
+
add_part(FormDataPart.new(name:, value:, headers:))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Adds a file to the multipart form data.
|
|
31
|
+
#
|
|
32
|
+
# @param name [String] The name of the form field
|
|
33
|
+
# @param file [String, #read] A path to a file on disk, or a file/readable object
|
|
34
|
+
# @param filename [String, nil] Optional filename (defaults to basename of the path for paths and File objects)
|
|
35
|
+
# @param content_type [String, nil] Optional content type (e.g. "image/png")
|
|
36
|
+
# @return [self] Returns self for chaining
|
|
37
|
+
def add_file(name:, file:, filename: nil, content_type: nil)
|
|
38
|
+
headers = content_type ? { "Content-Type" => content_type } : nil
|
|
39
|
+
if file.is_a?(::String)
|
|
40
|
+
filename ||= ::File.basename(file)
|
|
41
|
+
file = ::File.binread(file)
|
|
42
|
+
else
|
|
43
|
+
filename ||= filename_for(file)
|
|
44
|
+
end
|
|
45
|
+
add_part(FormDataPart.new(name:, value: file, filename:, headers:))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Adds a pre-created part to the multipart form data.
|
|
49
|
+
#
|
|
50
|
+
# @param part [FormDataPart] The part to add
|
|
51
|
+
# @return [self] Returns self for chaining
|
|
52
|
+
def add_part(part)
|
|
53
|
+
@parts << part
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Gets the content type string including the boundary.
|
|
58
|
+
#
|
|
59
|
+
# @return [String] The content type with boundary.
|
|
60
|
+
def content_type
|
|
61
|
+
@encoder.content_type
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Encode the multipart form data into a multipart/form-data payload.
|
|
65
|
+
#
|
|
66
|
+
# @return [String] The encoded body.
|
|
67
|
+
def encode
|
|
68
|
+
@encoder.encode(self)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def filename_for(file)
|
|
74
|
+
if file.is_a?(::File) || file.respond_to?(:path)
|
|
75
|
+
::File.basename(file.path)
|
|
76
|
+
elsif file.respond_to?(:name)
|
|
77
|
+
file.name
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module DexaAiSdk
|
|
6
|
+
module Internal
|
|
7
|
+
module Multipart
|
|
8
|
+
# @api private
|
|
9
|
+
class FormDataPart
|
|
10
|
+
attr_reader :name, :contents, :filename, :headers
|
|
11
|
+
|
|
12
|
+
# @param name [String] The name of the form field
|
|
13
|
+
# @param value [String, Integer, Float, Boolean, File, #read] The value of the field
|
|
14
|
+
# @param filename [String, nil] Optional filename for file uploads
|
|
15
|
+
# @param headers [Hash<String, String>, nil] Optional additional headers
|
|
16
|
+
def initialize(name:, value:, filename: nil, headers: nil)
|
|
17
|
+
@name = name
|
|
18
|
+
@contents = convert_to_content(value)
|
|
19
|
+
@filename = filename
|
|
20
|
+
@headers = headers
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Converts the part to a hash suitable for serialization.
|
|
24
|
+
#
|
|
25
|
+
# @return [Hash] A hash representation of the part
|
|
26
|
+
def to_hash
|
|
27
|
+
result = {
|
|
28
|
+
name: @name,
|
|
29
|
+
contents: @contents
|
|
30
|
+
}
|
|
31
|
+
result[:filename] = @filename if @filename
|
|
32
|
+
result[:headers] = @headers if @headers
|
|
33
|
+
result
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# Converts various types of values to a content representation
|
|
39
|
+
# @param value [String, Integer, Float, Boolean, #read] The value to convert
|
|
40
|
+
# @return [String] The string representation of the value
|
|
41
|
+
def convert_to_content(value)
|
|
42
|
+
if value.respond_to?(:read)
|
|
43
|
+
value.read
|
|
44
|
+
else
|
|
45
|
+
value.to_s
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DexaAiSdk
|
|
4
|
+
module Internal
|
|
5
|
+
module Multipart
|
|
6
|
+
# @api private
|
|
7
|
+
class Request < DexaAiSdk::Internal::Http::BaseRequest
|
|
8
|
+
attr_reader :body
|
|
9
|
+
|
|
10
|
+
# @param base_url [String] The base URL for the request
|
|
11
|
+
# @param path [String] The path for the request
|
|
12
|
+
# @param method [Symbol] The HTTP method for the request (:get, :post, etc.)
|
|
13
|
+
# @param headers [Hash] Additional headers for the request (optional)
|
|
14
|
+
# @param query [Hash] Query parameters for the request (optional)
|
|
15
|
+
# @param body [MultipartFormData, nil] The multipart form data for the request (optional)
|
|
16
|
+
# @param request_options [DexaAiSdk::RequestOptions, Hash{Symbol=>Object}, nil]
|
|
17
|
+
def initialize(base_url:, path:, method:, headers: {}, query: {}, body: nil, request_options: {})
|
|
18
|
+
super(base_url:, path:, method:, headers:, query:, request_options:)
|
|
19
|
+
|
|
20
|
+
@body = body
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Hash] The encoded HTTP request headers.
|
|
24
|
+
# @param protected_keys [Array<String>] Header keys set by the SDK client (e.g. auth, metadata)
|
|
25
|
+
# that must not be overridden by additional_headers from request_options.
|
|
26
|
+
def encode_headers(protected_keys: [])
|
|
27
|
+
sdk_headers = {
|
|
28
|
+
"Content-Type" => @body.content_type
|
|
29
|
+
}.merge(@headers)
|
|
30
|
+
merge_additional_headers(sdk_headers, protected_keys:)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [String, nil] The encoded HTTP request body.
|
|
34
|
+
# Note: additional_body_parameters from request_options are not applied to multipart requests.
|
|
35
|
+
def encode_body
|
|
36
|
+
@body&.encode
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DexaAiSdk
|
|
4
|
+
module Internal
|
|
5
|
+
module Types
|
|
6
|
+
# An array of a specific type
|
|
7
|
+
class Array
|
|
8
|
+
include DexaAiSdk::Internal::Types::Type
|
|
9
|
+
|
|
10
|
+
attr_reader :type
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# Instantiates a new `Array` of a given type
|
|
14
|
+
#
|
|
15
|
+
# @param type [Object] The member type of this array
|
|
16
|
+
#
|
|
17
|
+
# @return [DexaAiSdk::Internal::Types::Array]
|
|
18
|
+
def [](type)
|
|
19
|
+
new(type)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @api private
|
|
24
|
+
def initialize(type)
|
|
25
|
+
@type = type
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Coerces a value into this array
|
|
29
|
+
#
|
|
30
|
+
# @param value [Object]
|
|
31
|
+
# @option strict [Boolean]
|
|
32
|
+
# @return [::Array]
|
|
33
|
+
def coerce(value, strict: strict?)
|
|
34
|
+
unless value.is_a?(::Array)
|
|
35
|
+
raise Errors::TypeError, "cannot coerce `#{value.class}` to Array<#{type}>" if strict
|
|
36
|
+
|
|
37
|
+
return value
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
value.map do |element|
|
|
41
|
+
Utils.coerce(type, element, strict: strict)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DexaAiSdk
|
|
4
|
+
module Internal
|
|
5
|
+
module Types
|
|
6
|
+
module Boolean
|
|
7
|
+
extend DexaAiSdk::Internal::Types::Union
|
|
8
|
+
|
|
9
|
+
member TrueClass
|
|
10
|
+
member FalseClass
|
|
11
|
+
|
|
12
|
+
# Overrides the base coercion method for enums to allow integer and string values to become booleans
|
|
13
|
+
#
|
|
14
|
+
# @param value [Object]
|
|
15
|
+
# @option strict [Boolean]
|
|
16
|
+
# @return [Object]
|
|
17
|
+
def self.coerce(value, strict: strict?)
|
|
18
|
+
case value
|
|
19
|
+
when TrueClass, FalseClass
|
|
20
|
+
return value
|
|
21
|
+
when Integer
|
|
22
|
+
return value == 1
|
|
23
|
+
when String
|
|
24
|
+
return %w[1 true].include?(value)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
raise Errors::TypeError, "cannot coerce `#{value.class}` to Boolean" if strict
|
|
28
|
+
|
|
29
|
+
value
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DexaAiSdk
|
|
4
|
+
module Internal
|
|
5
|
+
module Types
|
|
6
|
+
# Module for defining enums
|
|
7
|
+
module Enum
|
|
8
|
+
include Type
|
|
9
|
+
|
|
10
|
+
# @api private
|
|
11
|
+
#
|
|
12
|
+
# @return [Array<Object>]
|
|
13
|
+
def values
|
|
14
|
+
@values ||= constants.map { |c| const_get(c) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @api private
|
|
18
|
+
def finalize!
|
|
19
|
+
values
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @api private
|
|
23
|
+
def strict?
|
|
24
|
+
@strict ||= false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @api private
|
|
28
|
+
def strict!
|
|
29
|
+
@strict = true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def coerce(value, strict: strict?)
|
|
33
|
+
coerced_value = Utils.coerce(Symbol, value)
|
|
34
|
+
|
|
35
|
+
return coerced_value if values.include?(coerced_value)
|
|
36
|
+
|
|
37
|
+
raise Errors::TypeError, "`#{value}` not in enum #{self}" if strict
|
|
38
|
+
|
|
39
|
+
value
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Parse JSON string and coerce to the enum value
|
|
43
|
+
#
|
|
44
|
+
# @param str [String] JSON string to parse
|
|
45
|
+
# @return [String] The enum value
|
|
46
|
+
def load(str)
|
|
47
|
+
coerce(::JSON.parse(str))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def inspect
|
|
51
|
+
"#{name}[#{values.join(", ")}]"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|