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
|
+
# Both reach the rest of the internal runtime transitively -- `URI` through `net/http`, `JSON`
|
|
4
|
+
# through the serializers -- and are required explicitly here because this file uses them
|
|
5
|
+
# directly: `encode_www_form` is its whole encoder, and `JSON.generate` renders non-scalar leaves.
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'uri'
|
|
8
|
+
|
|
9
|
+
module Petstore
|
|
10
|
+
module Internal
|
|
11
|
+
module Form
|
|
12
|
+
# A request whose body is a STRUCTURED value sent as `application/x-www-form-urlencoded`.
|
|
13
|
+
#
|
|
14
|
+
# FSDK-1441 routed a declared non-JSON body to `Internal::Raw::Request` only when its schema
|
|
15
|
+
# was a string or binary, because those travel verbatim. A urlencoded body's schema is an
|
|
16
|
+
# OBJECT, so it stayed on `Internal::JSON::Request` — which `JSON.generate`s it and labels it
|
|
17
|
+
# `application/json`. The server was told the wrong media type and handed a JSON document
|
|
18
|
+
# where it asked for form fields (FSDK-1668).
|
|
19
|
+
#
|
|
20
|
+
# @api private
|
|
21
|
+
class Request < ::Petstore::Internal::Http::BaseRequest
|
|
22
|
+
CONTENT_TYPE = 'application/x-www-form-urlencoded'
|
|
23
|
+
|
|
24
|
+
attr_reader :body
|
|
25
|
+
|
|
26
|
+
# @param base_url [String] The base URL for the request
|
|
27
|
+
# @param path [String] The path for the request
|
|
28
|
+
# @param method [Symbol] The HTTP method for the request (:get, :post, etc.)
|
|
29
|
+
# @param headers [Hash] Additional headers for the request (optional)
|
|
30
|
+
# @param query [Hash] Query parameters for the request (optional)
|
|
31
|
+
# @param body [Hash, nil] The request body as the model's wire hash (optional)
|
|
32
|
+
# @param content_type [String] The media type declared for the request body.
|
|
33
|
+
# @param accept [String] The media type declared for the response body.
|
|
34
|
+
# @param request_options [::Petstore::RequestOptions, Hash{Symbol=>Object}, nil]
|
|
35
|
+
def initialize(base_url:, path:, method:, headers: {}, query: {}, body: nil, content_type: CONTENT_TYPE, accept: '*/*', request_options: {})
|
|
36
|
+
super(base_url:, path:, method:, headers:, query:, request_options:)
|
|
37
|
+
|
|
38
|
+
@body = body
|
|
39
|
+
@content_type = content_type
|
|
40
|
+
@accept = accept
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [Hash] The encoded HTTP request headers.
|
|
44
|
+
# @param protected_keys [Array<String>] Header keys set by the SDK client (e.g. auth, metadata)
|
|
45
|
+
# that must not be overridden by additional_headers from request_options.
|
|
46
|
+
def encode_headers(protected_keys: [])
|
|
47
|
+
sdk_headers = {
|
|
48
|
+
'Content-Type' => @content_type,
|
|
49
|
+
'Accept' => @accept,
|
|
50
|
+
}.merge(@headers)
|
|
51
|
+
merge_additional_headers(sdk_headers, protected_keys:)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [String, nil] The encoded HTTP request body.
|
|
55
|
+
#
|
|
56
|
+
# MEMOISED for the same reason as the raw and multipart encoders: `RawClient#send` calls
|
|
57
|
+
# this once per attempt, so a retry must replay the bytes rather than re-derive them.
|
|
58
|
+
def encode_body
|
|
59
|
+
return nil if @body.nil?
|
|
60
|
+
# The memo is named after the METHOD rather than after the value it holds, which is the
|
|
61
|
+
# naming convention the generated .rubocop.yml enforces.
|
|
62
|
+
return @encode_body if defined?(@encode_body)
|
|
63
|
+
|
|
64
|
+
@encode_body =
|
|
65
|
+
if @body.is_a?(::Hash)
|
|
66
|
+
::URI.encode_www_form(form_fields)
|
|
67
|
+
else
|
|
68
|
+
# A oneOf/anyOf body can resolve to a bare scalar or an array rather than an object,
|
|
69
|
+
# and a form field has no name to hang one of those on. Passed through as its own
|
|
70
|
+
# text, which is what the raw path does for a urlencoded body declared as a string.
|
|
71
|
+
# Tested with +is_a?(::Hash)+ rather than +respond_to?(:to_h)+ because +Array#to_h+
|
|
72
|
+
# EXISTS and raises TypeError on a non-pair array.
|
|
73
|
+
@body.to_s
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# The model's wire hash flattened to the `[key, value]` pairs `URI.encode_www_form` is
|
|
80
|
+
# defined over, under the OpenAPI default for a urlencoded body (`form`, `explode: true`).
|
|
81
|
+
#
|
|
82
|
+
# An ARRAY becomes one repeated key per element, which is what `explode: true` means; it is
|
|
83
|
+
# expanded here rather than left to `encode_www_form`'s own array handling so an array of
|
|
84
|
+
# non-scalars gets the JSON leaf treatment below instead of `Array#to_s`.
|
|
85
|
+
#
|
|
86
|
+
# A nil value is OMITTED. `application/x-www-form-urlencoded` has no null, so the only
|
|
87
|
+
# alternatives are dropping the field or asserting an empty string for it, and an empty
|
|
88
|
+
# string is a different value the caller did not send.
|
|
89
|
+
def form_fields
|
|
90
|
+
@body.each_with_object([]) do |(key, value), pairs|
|
|
91
|
+
next if value.nil?
|
|
92
|
+
|
|
93
|
+
if value.is_a?(::Array)
|
|
94
|
+
value.each { |element| pairs << [key.to_s, form_value(element)] }
|
|
95
|
+
else
|
|
96
|
+
pairs << [key.to_s, form_value(value)]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# A form field is a scalar. OpenAPI says nothing about what a nested object or array leaf
|
|
102
|
+
# looks like inside a urlencoded body, so JSON is emitted for it — the caller's data
|
|
103
|
+
# survives, rather than Ruby's `Hash#to_s` / `Array#to_s` rendering reaching the wire.
|
|
104
|
+
def form_value(value)
|
|
105
|
+
value.is_a?(::Hash) || value.is_a?(::Array) ? ::JSON.generate(value) : value.to_s
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Petstore
|
|
4
|
+
module Internal
|
|
5
|
+
module Http
|
|
6
|
+
# @api private
|
|
7
|
+
class BaseRequest
|
|
8
|
+
attr_reader :base_url, :path, :method, :headers, :query, :request_options
|
|
9
|
+
|
|
10
|
+
# @param base_url [String] The base URL for the request
|
|
11
|
+
# @param path [String] The path for the request
|
|
12
|
+
# @param method [String] 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 request_options [::Petstore::RequestOptions, Hash{Symbol=>Object}, nil]
|
|
16
|
+
def initialize(base_url:, path:, method:, headers: {}, query: {}, request_options: {})
|
|
17
|
+
@base_url = base_url
|
|
18
|
+
@path = path
|
|
19
|
+
@method = method
|
|
20
|
+
@headers = headers
|
|
21
|
+
@query = query
|
|
22
|
+
@request_options = request_options
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The query parameters merged with additional query parameters from request options.
|
|
26
|
+
#
|
|
27
|
+
# A service passes +query+ as a Hash when every declared parameter is a default-style
|
|
28
|
+
# (form + explode) scalar or flat array, which +URI.encode_www_form+ already serializes
|
|
29
|
+
# correctly. When any parameter declares another style — spaceDelimited, pipeDelimited,
|
|
30
|
+
# deepObject, explode: false — or carries an object, the service passes an Array of
|
|
31
|
+
# already-encoded "key=value" parts instead, because those forms cannot be expressed as a
|
|
32
|
+
# Hash that +encode_www_form+ would render right. Both shapes are carried through to
|
|
33
|
+
# +RawClient#encode_query+, which joins an Array and form-encodes a Hash.
|
|
34
|
+
#
|
|
35
|
+
# @return [Hash, Array<String>] The merged query parameters.
|
|
36
|
+
def encode_query
|
|
37
|
+
additional_query = @request_options&.dig(:additional_query_parameters) || @request_options&.dig('additional_query_parameters') || {}
|
|
38
|
+
return @query.merge(additional_query) unless @query.is_a?(::Array)
|
|
39
|
+
|
|
40
|
+
# Braces, not do/end: `do` would bind to `+` rather than to `flat_map`.
|
|
41
|
+
@query + additional_query.flat_map { |key, value| ::Petstore::Serializers::Params.query_part(key.to_s, value) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Child classes should implement:
|
|
45
|
+
# - encode_headers: Returns the encoded HTTP request headers.
|
|
46
|
+
# - encode_body: Returns the encoded HTTP request body.
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Merges additional_headers from request_options into sdk_headers, filtering out
|
|
51
|
+
# any keys that collide with SDK-set or client-protected headers (case-insensitive).
|
|
52
|
+
# @param sdk_headers [Hash] Headers set by the SDK for this request type.
|
|
53
|
+
# @param protected_keys [Array<String>] Additional header keys that must not be overridden.
|
|
54
|
+
# @return [Hash] The merged headers.
|
|
55
|
+
def merge_additional_headers(sdk_headers, protected_keys: [])
|
|
56
|
+
additional_headers = @request_options&.dig(:additional_headers) || @request_options&.dig('additional_headers') || {}
|
|
57
|
+
all_protected = (sdk_headers.keys + protected_keys).to_set { |k| k.to_s.downcase }
|
|
58
|
+
filtered = additional_headers.reject { |key, _| all_protected.include?(key.to_s.downcase) }
|
|
59
|
+
sdk_headers.merge(filtered)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Petstore
|
|
4
|
+
module Internal
|
|
5
|
+
module Http
|
|
6
|
+
# Decoding a `text/event-stream` body into the events it carries.
|
|
7
|
+
#
|
|
8
|
+
# An SSE body is a sequence of frames, not one document: `JSON.parse` on the whole thing
|
|
9
|
+
# raises, so a service method that returned `response.body` for an event-stream response
|
|
10
|
+
# handed the caller the literal wire text (`"data: {...}\n\n"`) with no way to reach the
|
|
11
|
+
# events inside it. This is the seam the default (non-fernMode) layout has had since it grew
|
|
12
|
+
# `HTTP::Connection#stream_execute`, expressed for the fernMode runtime.
|
|
13
|
+
#
|
|
14
|
+
# The frame grammar is the SSE spec's, which is a superset of what `stream_execute` handles.
|
|
15
|
+
# `stream_execute` treats every `data:` LINE as its own event, so the two agree exactly on
|
|
16
|
+
# the common one-line-per-frame shape; where they differ, a multi-line `data` frame joins
|
|
17
|
+
# here into the one event the spec says it is, instead of being fed to `JSON.parse` a
|
|
18
|
+
# fragment at a time.
|
|
19
|
+
#
|
|
20
|
+
# @api private
|
|
21
|
+
module EventStream
|
|
22
|
+
# The events in an event-stream body, each passed through the given block.
|
|
23
|
+
#
|
|
24
|
+
# Returns an Enumerator rather than an Array so `#first` on a long stream decodes one frame
|
|
25
|
+
# rather than all of them, and so the shape matches what the default layout's streaming
|
|
26
|
+
# methods return.
|
|
27
|
+
#
|
|
28
|
+
# The terminator is the API's own, passed from the spec rather than assumed. A nil
|
|
29
|
+
# terminator means the API declared none, so NO payload ends the stream early -- a service
|
|
30
|
+
# whose events legitimately include a `[DONE]`-shaped payload must not be truncated on it.
|
|
31
|
+
# Matched exactly when present, so a payload that merely resembles it is still delivered.
|
|
32
|
+
#
|
|
33
|
+
# @param response [Net::HTTPResponse] The HTTP response.
|
|
34
|
+
# @param terminator [String, nil] The declared end-of-stream payload, if any.
|
|
35
|
+
# @return [Enumerator] The decoded events, lazily.
|
|
36
|
+
def self.decode(response, terminator: nil)
|
|
37
|
+
# `to_s` because a 2xx is entitled to arrive with no body at all, which is an empty
|
|
38
|
+
# stream rather than an error -- the same case `ResponseBody.decode` guards.
|
|
39
|
+
body = response.body.to_s
|
|
40
|
+
::Enumerator.new do |yielder|
|
|
41
|
+
# Frames are separated by a blank line. A CR is tolerated because SSE is CRLF on the
|
|
42
|
+
# wire, and a body split on "\n\n" alone would leave a trailing "\r" on every field.
|
|
43
|
+
body.split(/\r?\n\r?\n/).each do |frame|
|
|
44
|
+
data = data_field(frame)
|
|
45
|
+
# A frame with an empty data payload is not dispatched. That covers a comment-only
|
|
46
|
+
# keepalive (`: ping`), a frame holding only `event`/`id`/`retry`, the trailing empty
|
|
47
|
+
# segment a body ending in a blank line splits into, and an explicitly-empty `data:`
|
|
48
|
+
# frame -- all of which would otherwise reach the caller's event type as an empty
|
|
49
|
+
# payload and raise `JSON::ParserError` from inside the SDK. See `data_field` for why
|
|
50
|
+
# the last of those is dropped even though the spec would dispatch it.
|
|
51
|
+
next if data.nil?
|
|
52
|
+
break if !terminator.nil? && data == terminator
|
|
53
|
+
|
|
54
|
+
yielder << yield(data)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The frame's `data` value: its `data:` lines joined with a newline, or nil when the frame
|
|
60
|
+
# has no data to dispatch. Multiple `data:` lines in one frame are ONE value per the spec,
|
|
61
|
+
# which is why this collects them instead of returning the first.
|
|
62
|
+
#
|
|
63
|
+
# The join IS the spec's data buffer: the spec appends each `data` field's value followed by
|
|
64
|
+
# a newline, then drops one trailing newline when it dispatches, which is what joining the
|
|
65
|
+
# values on a newline produces directly.
|
|
66
|
+
#
|
|
67
|
+
# @param frame [String] A single SSE event frame.
|
|
68
|
+
# @return [String, nil] The frame's data value, or nil when it carries none.
|
|
69
|
+
def self.data_field(frame)
|
|
70
|
+
values = []
|
|
71
|
+
frame.split(/\r?\n/).each do |line|
|
|
72
|
+
# Strip the CR before anything else: a bare `data\r` line carries no colon, and
|
|
73
|
+
# `partition` would otherwise read the field name as `data\r` and miss it.
|
|
74
|
+
line = line.chomp("\r")
|
|
75
|
+
# A blank line is not a field, and a leading colon is a comment.
|
|
76
|
+
next if line.empty? || line.start_with?(':')
|
|
77
|
+
|
|
78
|
+
name, colon, value = line.partition(':')
|
|
79
|
+
next unless name == 'data'
|
|
80
|
+
|
|
81
|
+
# A field with no colon has an empty value; otherwise a single leading space after the
|
|
82
|
+
# colon is part of the framing rather than the payload.
|
|
83
|
+
values << (colon.empty? ? '' : value.delete_prefix(' '))
|
|
84
|
+
end
|
|
85
|
+
data = values.join("\n")
|
|
86
|
+
# An EMPTY payload is dropped DELIBERATELY, and this is the one place the decoder departs
|
|
87
|
+
# from the spec: the spec checks its buffer for emptiness BEFORE dropping the trailing
|
|
88
|
+
# newline, so a lone `data:` frame is dispatchable there with an empty payload. It is not
|
|
89
|
+
# dispatchable here, because an empty string is not a document any event type can load --
|
|
90
|
+
# `JSON.parse('')` raises `JSON::ParserError` from inside the SDK, which is the failure
|
|
91
|
+
# this seam exists to prevent -- and in practice an empty `data:` frame is a keepalive,
|
|
92
|
+
# the same intent as the `:` comment form. The test is on the JOINED payload rather than
|
|
93
|
+
# on `values`, so "no data field at all" and "an explicitly empty data field" collapse to
|
|
94
|
+
# the same nil on purpose: for this decoder both mean nothing to dispatch. Anything
|
|
95
|
+
# non-empty is passed through untouched, so a two-line `data:`/`data:` frame still
|
|
96
|
+
# reaches the caller as the "\n" the spec says it is.
|
|
97
|
+
data.empty? ? nil : data
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Private because `decode` is the documented seam; the frame grammar is an implementation
|
|
101
|
+
# detail that callers must not be able to reach for.
|
|
102
|
+
private_class_method :data_field
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|