fsdk-2237-ruby-e2e 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +107 -0
- data/lib/petstore/client.rb +45 -0
- data/lib/petstore/config.rb +54 -0
- data/lib/petstore/environment.rb +7 -0
- data/lib/petstore/error.rb +40 -0
- data/lib/petstore/errors/api_error.rb +9 -0
- data/lib/petstore/errors/client_error.rb +43 -0
- data/lib/petstore/errors/connection_error.rb +11 -0
- data/lib/petstore/errors/redirect_error.rb +9 -0
- data/lib/petstore/errors/response_error.rb +92 -0
- data/lib/petstore/errors/server_error.rb +25 -0
- data/lib/petstore/errors/timeout_error.rb +9 -0
- data/lib/petstore/http/auth.rb +19 -0
- data/lib/petstore/http/oauth_token_manager.rb +91 -0
- data/lib/petstore/internal/errors/constraint_error.rb +10 -0
- data/lib/petstore/internal/errors/type_error.rb +10 -0
- data/lib/petstore/internal/form/request.rb +110 -0
- data/lib/petstore/internal/http/base_request.rb +64 -0
- data/lib/petstore/internal/http/event_stream.rb +106 -0
- data/lib/petstore/internal/http/raw_client.rb +426 -0
- data/lib/petstore/internal/http/response_body.rb +35 -0
- data/lib/petstore/internal/iterators/cursor_item_iterator.rb +28 -0
- data/lib/petstore/internal/iterators/cursor_page_iterator.rb +72 -0
- data/lib/petstore/internal/iterators/item_iterator.rb +72 -0
- data/lib/petstore/internal/iterators/offset_item_iterator.rb +30 -0
- data/lib/petstore/internal/iterators/offset_page_iterator.rb +110 -0
- data/lib/petstore/internal/json/request.rb +61 -0
- data/lib/petstore/internal/json/serializable.rb +25 -0
- data/lib/petstore/internal/multipart/multipart_encoder.rb +141 -0
- data/lib/petstore/internal/multipart/multipart_form_data.rb +78 -0
- data/lib/petstore/internal/multipart/multipart_form_data_part.rb +51 -0
- data/lib/petstore/internal/multipart/multipart_request.rb +49 -0
- data/lib/petstore/internal/raw/request.rb +88 -0
- data/lib/petstore/internal/types/array.rb +47 -0
- data/lib/petstore/internal/types/boolean.rb +34 -0
- data/lib/petstore/internal/types/enum.rb +56 -0
- data/lib/petstore/internal/types/hash.rb +36 -0
- data/lib/petstore/internal/types/model/field.rb +38 -0
- data/lib/petstore/internal/types/model.rb +246 -0
- data/lib/petstore/internal/types/type.rb +35 -0
- data/lib/petstore/internal/types/union.rb +193 -0
- data/lib/petstore/internal/types/unknown.rb +15 -0
- data/lib/petstore/internal/types/utils.rb +137 -0
- data/lib/petstore/models/open_model.rb +30 -0
- data/lib/petstore/models/serializable.rb +63 -0
- data/lib/petstore/models/union_type.rb +12 -0
- data/lib/petstore/pet/client.rb +196 -0
- data/lib/petstore/pet/types/delete_pet_request.rb +13 -0
- data/lib/petstore/pet/types/find_pets_by_status_request.rb +11 -0
- data/lib/petstore/pet/types/find_pets_by_status_request_status.rb +24 -0
- data/lib/petstore/pet/types/find_pets_by_tags_request.rb +11 -0
- data/lib/petstore/pet/types/get_pet_by_id_request.rb +11 -0
- data/lib/petstore/pet/types/pet.rb +25 -0
- data/lib/petstore/pet/types/update_pet_with_form_request.rb +15 -0
- data/lib/petstore/serializers/form_serializer.rb +41 -0
- data/lib/petstore/serializers/json_serializer.rb +41 -0
- data/lib/petstore/serializers/xml_serializer.rb +56 -0
- data/lib/petstore/store/client.rb +89 -0
- data/lib/petstore/store/types/delete_order_request.rb +11 -0
- data/lib/petstore/store/types/get_order_by_id_request.rb +11 -0
- data/lib/petstore/store/types/order.rb +25 -0
- data/lib/petstore/types/api_response.rb +20 -0
- data/lib/petstore/types/category.rb +19 -0
- data/lib/petstore/types/order_status.rb +22 -0
- data/lib/petstore/types/pet_status.rb +22 -0
- data/lib/petstore/types/tag.rb +19 -0
- data/lib/petstore/user/client.rb +149 -0
- data/lib/petstore/user/types/delete_user_request.rb +11 -0
- data/lib/petstore/user/types/get_user_by_name_request.rb +11 -0
- data/lib/petstore/user/types/login_user_request.rb +13 -0
- data/lib/petstore/user/types/user.rb +27 -0
- data/lib/petstore/validator.rb +42 -0
- data/lib/petstore/version.rb +5 -0
- data/lib/petstore.rb +78 -0
- metadata +236 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Petstore
|
|
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 [::Petstore::Internal::OffsetPageIterator]
|
|
21
|
+
def initialize(initial_page:, item_field:, has_next_field:, step:, &block)
|
|
22
|
+
@page_number = 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
|
+
|
|
28
|
+
# A cache of whether the API has another page, if it gives us that information...
|
|
29
|
+
@next_page = nil
|
|
30
|
+
# ...or the actual next page, preloaded, if it doesn't.
|
|
31
|
+
@has_next_page = nil
|
|
32
|
+
|
|
33
|
+
@http_response = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Iterates over each page returned by the API.
|
|
37
|
+
#
|
|
38
|
+
# @yield [page] Each retrieved page.
|
|
39
|
+
# @return [NilClass]
|
|
40
|
+
def each
|
|
41
|
+
while (page = next_page)
|
|
42
|
+
yield(page)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Whether another page will be available from the API.
|
|
47
|
+
#
|
|
48
|
+
# @return [Boolean]
|
|
49
|
+
def next?
|
|
50
|
+
return @has_next_page unless @has_next_page.nil?
|
|
51
|
+
return true if @next_page
|
|
52
|
+
|
|
53
|
+
fetched_page = fetch_page(@page_number)
|
|
54
|
+
fetched_page_items = resolve_field(fetched_page, @item_field)
|
|
55
|
+
if fetched_page_items.nil? || fetched_page_items.empty?
|
|
56
|
+
@has_next_page = false
|
|
57
|
+
else
|
|
58
|
+
@next_page = fetched_page
|
|
59
|
+
true
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns the next page from the API.
|
|
64
|
+
def next_page
|
|
65
|
+
return nil if @page_number.nil?
|
|
66
|
+
|
|
67
|
+
if @next_page
|
|
68
|
+
this_page = @next_page
|
|
69
|
+
@next_page = nil
|
|
70
|
+
else
|
|
71
|
+
this_page = fetch_page(@page_number)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
@has_next_page = resolve_field(this_page, @has_next_field) if @has_next_field
|
|
75
|
+
|
|
76
|
+
items = resolve_field(this_page, @item_field)
|
|
77
|
+
if items.nil? || items.empty?
|
|
78
|
+
@page_number = nil
|
|
79
|
+
return nil
|
|
80
|
+
elsif @step
|
|
81
|
+
@page_number += items.length
|
|
82
|
+
else
|
|
83
|
+
@page_number += 1
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
this_page
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# Resolves a field accessor (Symbol) or nested accessor path (Array<Symbol>) against a page.
|
|
92
|
+
def resolve_field(page, field)
|
|
93
|
+
return nil if page.nil? || field.nil?
|
|
94
|
+
|
|
95
|
+
Array(field).reduce(page) { |value, accessor| value&.send(accessor) }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def fetch_page(page_number)
|
|
99
|
+
result = @get_next_page.call(page_number)
|
|
100
|
+
if result.is_a?(Array)
|
|
101
|
+
fetched_page, raw_response = result
|
|
102
|
+
@http_response = raw_response
|
|
103
|
+
fetched_page
|
|
104
|
+
else
|
|
105
|
+
result
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Petstore
|
|
4
|
+
module Internal
|
|
5
|
+
module JSON
|
|
6
|
+
# @api private
|
|
7
|
+
class Request < ::Petstore::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 content_type [String] The media type declared for the request body. JSON-family
|
|
17
|
+
# types are not interchangeable: `application/merge-patch+json` selects merge-patch
|
|
18
|
+
# semantics, and sending `application/json` instead changes what the server does.
|
|
19
|
+
# @param accept [String] The media type declared for the RESPONSE body. A server that
|
|
20
|
+
# content-negotiates on Accept can 406 a request that asks for the wrong one, so an
|
|
21
|
+
# endpoint returning `application/vnd.api+json` has to say so.
|
|
22
|
+
# @param request_options [::Petstore::RequestOptions, Hash{Symbol=>Object}, nil]
|
|
23
|
+
def initialize(base_url:, path:, method:, headers: {}, query: {}, body: nil, content_type: 'application/json', accept: 'application/json', request_options: {})
|
|
24
|
+
super(base_url:, path:, method:, headers:, query:, request_options:)
|
|
25
|
+
|
|
26
|
+
@body = body
|
|
27
|
+
@content_type = content_type
|
|
28
|
+
@accept = accept
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [Hash] The encoded HTTP request headers.
|
|
32
|
+
# @param protected_keys [Array<String>] Header keys set by the SDK client (e.g. auth, metadata)
|
|
33
|
+
# that must not be overridden by additional_headers from request_options.
|
|
34
|
+
def encode_headers(protected_keys: [])
|
|
35
|
+
sdk_headers = {
|
|
36
|
+
'Content-Type' => @content_type,
|
|
37
|
+
'Accept' => @accept,
|
|
38
|
+
}.merge(@headers)
|
|
39
|
+
merge_additional_headers(sdk_headers, protected_keys:)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [String, nil] The encoded HTTP request body.
|
|
43
|
+
#
|
|
44
|
+
# `additional_body_parameters` from request_options is the per-request escape hatch for
|
|
45
|
+
# sending undocumented or beta fields without a new SDK version. It is shallow-merged onto
|
|
46
|
+
# an object body with the extras winning, so an extra key named like a declared field
|
|
47
|
+
# replaces it. A nil body becomes the extras; an array or primitive body has no object to
|
|
48
|
+
# merge onto and is sent unchanged. Keys are stringified because a model's `#to_h` is keyed
|
|
49
|
+
# by wire name (a String) and a Symbol key would otherwise emit the field twice.
|
|
50
|
+
def encode_body
|
|
51
|
+
extra = @request_options&.dig(:additional_body_parameters) || @request_options&.dig('additional_body_parameters')
|
|
52
|
+
return @body.nil? ? nil : ::JSON.generate(@body) unless extra.is_a?(::Hash) && !extra.empty?
|
|
53
|
+
return ::JSON.generate(extra.transform_keys(&:to_s)) if @body.nil?
|
|
54
|
+
return ::JSON.generate(@body) unless @body.is_a?(::Hash)
|
|
55
|
+
|
|
56
|
+
::JSON.generate(@body.merge(extra.transform_keys(&:to_s)))
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Petstore
|
|
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 Petstore
|
|
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 = +''
|
|
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,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Petstore
|
|
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 [#read] The file or readable object
|
|
34
|
+
# @param filename [String, nil] Optional filename (defaults to basename of path for 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
|
+
filename ||= filename_for(file)
|
|
40
|
+
add_part(FormDataPart.new(name:, value: file, filename:, headers:))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Adds a pre-created part to the multipart form data.
|
|
44
|
+
#
|
|
45
|
+
# @param part [FormDataPart] The part to add
|
|
46
|
+
# @return [self] Returns self for chaining
|
|
47
|
+
def add_part(part)
|
|
48
|
+
@parts << part
|
|
49
|
+
self
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Gets the content type string including the boundary.
|
|
53
|
+
#
|
|
54
|
+
# @return [String] The content type with boundary.
|
|
55
|
+
def content_type
|
|
56
|
+
@encoder.content_type
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Encode the multipart form data into a multipart/form-data payload.
|
|
60
|
+
#
|
|
61
|
+
# @return [String] The encoded body.
|
|
62
|
+
def encode
|
|
63
|
+
@encoder.encode(self)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def filename_for(file)
|
|
69
|
+
if file.is_a?(::File) || file.respond_to?(:path)
|
|
70
|
+
::File.basename(file.path)
|
|
71
|
+
elsif file.respond_to?(:name)
|
|
72
|
+
file.name
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Petstore
|
|
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,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Petstore
|
|
4
|
+
module Internal
|
|
5
|
+
module Multipart
|
|
6
|
+
# @api private
|
|
7
|
+
class Request < ::Petstore::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 [::Petstore::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
|
+
#
|
|
35
|
+
# MEMOISED for the same reason as `Raw::Request#encode_body`: `RawClient#send` encodes once
|
|
36
|
+
# per attempt, and `MultipartEncoder#close` marks the encoder closed — so a second `encode`
|
|
37
|
+
# raised `RuntimeError: Cannot write to closed encoder`. A retried multipart upload (a POST
|
|
38
|
+
# carrying an idempotency key) therefore died inside the SDK instead of being replayed.
|
|
39
|
+
def encode_body
|
|
40
|
+
# The memo is named after the METHOD rather than after the value it holds, which is the
|
|
41
|
+
# naming convention the generated .rubocop.yml enforces.
|
|
42
|
+
return @encode_body if defined?(@encode_body)
|
|
43
|
+
|
|
44
|
+
@encode_body = @body&.encode
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Petstore
|
|
4
|
+
module Internal
|
|
5
|
+
module Raw
|
|
6
|
+
# A request whose body is sent to the server exactly as the caller supplied it.
|
|
7
|
+
#
|
|
8
|
+
# `Internal::JSON::Request` is the wrong carrier for a declared non-JSON body: `encode_body`
|
|
9
|
+
# runs `::JSON.generate`, which turns a `text/plain` string into a quoted JSON string
|
|
10
|
+
# (`"hello"` on the wire, not `hello`) and RAISES on binary bytes
|
|
11
|
+
# (`Encoding::UndefinedConversionError`) — so an `application/octet-stream` upload failed
|
|
12
|
+
# inside the SDK's own encoder before it ever reached the network. With the body left
|
|
13
|
+
# untouched here, the declared media type in `Content-Type` is finally the truth about what
|
|
14
|
+
# was sent, which is what let FSDK-1442 stop defaulting a non-JSON type to `application/json`.
|
|
15
|
+
#
|
|
16
|
+
# @api private
|
|
17
|
+
class Request < ::Petstore::Internal::Http::BaseRequest
|
|
18
|
+
attr_reader :body
|
|
19
|
+
|
|
20
|
+
# @param base_url [String] The base URL for the request
|
|
21
|
+
# @param path [String] The path for the request
|
|
22
|
+
# @param method [Symbol] The HTTP method for the request (:get, :post, etc.)
|
|
23
|
+
# @param headers [Hash] Additional headers for the request (optional)
|
|
24
|
+
# @param query [Hash] Query parameters for the request (optional)
|
|
25
|
+
# @param body [String, #read, nil] The raw request body (optional)
|
|
26
|
+
# @param content_type [String] The media type declared for the request body.
|
|
27
|
+
# @param accept [String] The media type declared for the response body.
|
|
28
|
+
# @param request_options [::Petstore::RequestOptions, Hash{Symbol=>Object}, nil]
|
|
29
|
+
def initialize(base_url:, path:, method:, headers: {}, query: {}, body: nil, content_type: 'application/octet-stream', accept: '*/*', request_options: {})
|
|
30
|
+
super(base_url:, path:, method:, headers:, query:, request_options:)
|
|
31
|
+
|
|
32
|
+
@body = body
|
|
33
|
+
@content_type = content_type
|
|
34
|
+
@accept = accept
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Hash] The encoded HTTP request headers.
|
|
38
|
+
# @param protected_keys [Array<String>] Header keys set by the SDK client (e.g. auth, metadata)
|
|
39
|
+
# that must not be overridden by additional_headers from request_options.
|
|
40
|
+
def encode_headers(protected_keys: [])
|
|
41
|
+
sdk_headers = {
|
|
42
|
+
'Content-Type' => @content_type,
|
|
43
|
+
'Accept' => @accept,
|
|
44
|
+
}.merge(@headers)
|
|
45
|
+
merge_additional_headers(sdk_headers, protected_keys:)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [String, nil] The encoded HTTP request body.
|
|
49
|
+
#
|
|
50
|
+
# An IO/File is READ rather than `to_s`-ed, because `File#to_s` is
|
|
51
|
+
# `"#<File:0x000...>"` — the object's inspect output, not its bytes. `binmode` first so the
|
|
52
|
+
# bytes arrive as ASCII-8BIT: a `File.open(path)` defaults to the external encoding, and a
|
|
53
|
+
# UTF-8-tagged string holding invalid UTF-8 raises when concatenated downstream.
|
|
54
|
+
# MEMOISED, because `RawClient#send` calls this once per attempt and an IO can only be
|
|
55
|
+
# drained once: on a retry the handle already sits at EOF, so attempt 2 read `nil`/`""` and
|
|
56
|
+
# the SDK uploaded an EMPTY body while reporting success. Reading once and replaying the
|
|
57
|
+
# bytes is what makes a raw upload survive a retry. `rewind` additionally covers a handle
|
|
58
|
+
# that arrives already partly consumed; a non-seekable IO (a pipe) refuses it, which is
|
|
59
|
+
# harmless here since it is read exactly once anyway.
|
|
60
|
+
def encode_body
|
|
61
|
+
return nil if @body.nil?
|
|
62
|
+
# The memo is named after the METHOD rather than after the value it holds, which is the
|
|
63
|
+
# naming convention the generated .rubocop.yml enforces.
|
|
64
|
+
return @encode_body if defined?(@encode_body)
|
|
65
|
+
|
|
66
|
+
@encode_body =
|
|
67
|
+
if @body.respond_to?(:read)
|
|
68
|
+
@body.binmode if @body.respond_to?(:binmode)
|
|
69
|
+
rewind_body
|
|
70
|
+
@body.read
|
|
71
|
+
elsif @body.is_a?(::String)
|
|
72
|
+
@body
|
|
73
|
+
else
|
|
74
|
+
@body.to_s
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def rewind_body
|
|
81
|
+
@body.rewind if @body.respond_to?(:rewind)
|
|
82
|
+
rescue ::IOError, ::SystemCallError
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Petstore
|
|
4
|
+
module Internal
|
|
5
|
+
module Types
|
|
6
|
+
# An array of a specific type
|
|
7
|
+
class Array
|
|
8
|
+
include ::Petstore::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 [::Petstore::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 Petstore
|
|
4
|
+
module Internal
|
|
5
|
+
module Types
|
|
6
|
+
module Boolean
|
|
7
|
+
extend ::Petstore::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
|