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
+ # 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