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.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +107 -0
  3. data/lib/petstore/client.rb +45 -0
  4. data/lib/petstore/config.rb +54 -0
  5. data/lib/petstore/environment.rb +7 -0
  6. data/lib/petstore/error.rb +40 -0
  7. data/lib/petstore/errors/api_error.rb +9 -0
  8. data/lib/petstore/errors/client_error.rb +43 -0
  9. data/lib/petstore/errors/connection_error.rb +11 -0
  10. data/lib/petstore/errors/redirect_error.rb +9 -0
  11. data/lib/petstore/errors/response_error.rb +92 -0
  12. data/lib/petstore/errors/server_error.rb +25 -0
  13. data/lib/petstore/errors/timeout_error.rb +9 -0
  14. data/lib/petstore/http/auth.rb +19 -0
  15. data/lib/petstore/http/oauth_token_manager.rb +91 -0
  16. data/lib/petstore/internal/errors/constraint_error.rb +10 -0
  17. data/lib/petstore/internal/errors/type_error.rb +10 -0
  18. data/lib/petstore/internal/form/request.rb +110 -0
  19. data/lib/petstore/internal/http/base_request.rb +64 -0
  20. data/lib/petstore/internal/http/event_stream.rb +106 -0
  21. data/lib/petstore/internal/http/raw_client.rb +426 -0
  22. data/lib/petstore/internal/http/response_body.rb +35 -0
  23. data/lib/petstore/internal/iterators/cursor_item_iterator.rb +28 -0
  24. data/lib/petstore/internal/iterators/cursor_page_iterator.rb +72 -0
  25. data/lib/petstore/internal/iterators/item_iterator.rb +72 -0
  26. data/lib/petstore/internal/iterators/offset_item_iterator.rb +30 -0
  27. data/lib/petstore/internal/iterators/offset_page_iterator.rb +110 -0
  28. data/lib/petstore/internal/json/request.rb +61 -0
  29. data/lib/petstore/internal/json/serializable.rb +25 -0
  30. data/lib/petstore/internal/multipart/multipart_encoder.rb +141 -0
  31. data/lib/petstore/internal/multipart/multipart_form_data.rb +78 -0
  32. data/lib/petstore/internal/multipart/multipart_form_data_part.rb +51 -0
  33. data/lib/petstore/internal/multipart/multipart_request.rb +49 -0
  34. data/lib/petstore/internal/raw/request.rb +88 -0
  35. data/lib/petstore/internal/types/array.rb +47 -0
  36. data/lib/petstore/internal/types/boolean.rb +34 -0
  37. data/lib/petstore/internal/types/enum.rb +56 -0
  38. data/lib/petstore/internal/types/hash.rb +36 -0
  39. data/lib/petstore/internal/types/model/field.rb +38 -0
  40. data/lib/petstore/internal/types/model.rb +246 -0
  41. data/lib/petstore/internal/types/type.rb +35 -0
  42. data/lib/petstore/internal/types/union.rb +193 -0
  43. data/lib/petstore/internal/types/unknown.rb +15 -0
  44. data/lib/petstore/internal/types/utils.rb +137 -0
  45. data/lib/petstore/models/open_model.rb +30 -0
  46. data/lib/petstore/models/serializable.rb +63 -0
  47. data/lib/petstore/models/union_type.rb +12 -0
  48. data/lib/petstore/pet/client.rb +196 -0
  49. data/lib/petstore/pet/types/delete_pet_request.rb +13 -0
  50. data/lib/petstore/pet/types/find_pets_by_status_request.rb +11 -0
  51. data/lib/petstore/pet/types/find_pets_by_status_request_status.rb +24 -0
  52. data/lib/petstore/pet/types/find_pets_by_tags_request.rb +11 -0
  53. data/lib/petstore/pet/types/get_pet_by_id_request.rb +11 -0
  54. data/lib/petstore/pet/types/pet.rb +25 -0
  55. data/lib/petstore/pet/types/update_pet_with_form_request.rb +15 -0
  56. data/lib/petstore/serializers/form_serializer.rb +41 -0
  57. data/lib/petstore/serializers/json_serializer.rb +41 -0
  58. data/lib/petstore/serializers/xml_serializer.rb +56 -0
  59. data/lib/petstore/store/client.rb +89 -0
  60. data/lib/petstore/store/types/delete_order_request.rb +11 -0
  61. data/lib/petstore/store/types/get_order_by_id_request.rb +11 -0
  62. data/lib/petstore/store/types/order.rb +25 -0
  63. data/lib/petstore/types/api_response.rb +20 -0
  64. data/lib/petstore/types/category.rb +19 -0
  65. data/lib/petstore/types/order_status.rb +22 -0
  66. data/lib/petstore/types/pet_status.rb +22 -0
  67. data/lib/petstore/types/tag.rb +19 -0
  68. data/lib/petstore/user/client.rb +149 -0
  69. data/lib/petstore/user/types/delete_user_request.rb +11 -0
  70. data/lib/petstore/user/types/get_user_by_name_request.rb +11 -0
  71. data/lib/petstore/user/types/login_user_request.rb +13 -0
  72. data/lib/petstore/user/types/user.rb +27 -0
  73. data/lib/petstore/validator.rb +42 -0
  74. data/lib/petstore/version.rb +5 -0
  75. data/lib/petstore.rb +78 -0
  76. 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