diff --git a/.github/workflows/codegen_ci.yml b/.github/workflows/codegen_ci.yml index 126a52408..3fc79bd36 100644 --- a/.github/workflows/codegen_ci.yml +++ b/.github/workflows/codegen_ci.yml @@ -42,7 +42,7 @@ jobs: ruby-rbs-type-check: needs: [generate-test-sdk] - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest strategy: fail-fast: false matrix: diff --git a/codegen/build.gradle.kts b/codegen/build.gradle.kts index d2c178b5b..e3dfda656 100644 --- a/codegen/build.gradle.kts +++ b/codegen/build.gradle.kts @@ -28,7 +28,7 @@ allprojects { version = "0.1.0" } -extra["smithyVersion"] = "1.26.0" +extra["smithyVersion"] = "1.28.0" // The root project doesn't produce a JAR. tasks["jar"].enabled = false diff --git a/codegen/projections/high_score_service/lib/high_score_service/builders.rb b/codegen/projections/high_score_service/lib/high_score_service/builders.rb index bfbaa5444..5edf793b5 100644 --- a/codegen/projections/high_score_service/lib/high_score_service/builders.rb +++ b/codegen/projections/high_score_service/lib/high_score_service/builders.rb @@ -8,6 +8,7 @@ # WARNING ABOUT GENERATED CODE module HighScoreService + # @api private module Builders # Operation Builder for CreateHighScore @@ -16,7 +17,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/high_scores') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = {} @@ -38,7 +39,7 @@ def self.build(http_req, input:) ) ) params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -55,7 +56,7 @@ def self.build(http_req, input:) ) ) params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -75,7 +76,7 @@ def self.build(http_req, input:) http_req.http_method = 'GET' http_req.append_path('/high_scores') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -92,7 +93,7 @@ def self.build(http_req, input:) ) ) params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = {} diff --git a/codegen/projections/high_score_service/lib/high_score_service/client.rb b/codegen/projections/high_score_service/lib/high_score_service/client.rb index 73c2ca2ee..8a85bf574 100644 --- a/codegen/projections/high_score_service/lib/high_score_service/client.rb +++ b/codegen/projections/high_score_service/lib/high_score_service/client.rb @@ -85,12 +85,8 @@ def create_high_score(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 201, errors: [Errors::UnprocessableEntityError]), @@ -99,7 +95,7 @@ def create_high_score(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::CreateHighScore, stubs: @stubs, params_class: Params::CreateHighScoreOutput @@ -109,7 +105,7 @@ def create_high_score(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -154,12 +150,8 @@ def delete_high_score(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -168,7 +160,7 @@ def delete_high_score(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::DeleteHighScore, stubs: @stubs, params_class: Params::DeleteHighScoreOutput @@ -178,7 +170,7 @@ def delete_high_score(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -228,12 +220,8 @@ def get_high_score(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -242,7 +230,7 @@ def get_high_score(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::GetHighScore, stubs: @stubs, params_class: Params::GetHighScoreOutput @@ -252,7 +240,7 @@ def get_high_score(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -298,12 +286,8 @@ def list_high_scores(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -312,7 +296,7 @@ def list_high_scores(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::ListHighScores, stubs: @stubs, params_class: Params::ListHighScoresOutput @@ -322,7 +306,7 @@ def list_high_scores(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -380,12 +364,8 @@ def update_high_score(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: [Errors::UnprocessableEntityError]), @@ -394,7 +374,7 @@ def update_high_score(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::UpdateHighScore, stubs: @stubs, params_class: Params::UpdateHighScoreOutput @@ -404,7 +384,7 @@ def update_high_score(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, diff --git a/codegen/projections/high_score_service/lib/high_score_service/config.rb b/codegen/projections/high_score_service/lib/high_score_service/config.rb index 71c6215d7..ca2acefa1 100644 --- a/codegen/projections/high_score_service/lib/high_score_service/config.rb +++ b/codegen/projections/high_score_service/lib/high_score_service/config.rb @@ -9,31 +9,27 @@ module HighScoreService # @!method initialize(*options) - # @option args [Boolean] :adaptive_retry_wait_to_fill (true) - # Used only in `adaptive` retry mode. When true, the request will sleep until there is sufficient client side capacity to retry the request. When false, the request will raise a `CapacityNotAvailableError` and will not retry instead of sleeping. - # # @option args [Boolean] :disable_host_prefix (false) # When `true`, does not perform host prefix injection using @endpoint's hostPrefix property. # # @option args [String] :endpoint # Endpoint of the service # - # @option args [Boolean] :http_wire_trace (false) - # Enable debug wire trace on http requests. + # @option args [Hearth::HTTP::Client] :http_client (Hearth::HTTP::Client.new) + # The HTTP Client to use for request transport. # # @option args [Symbol] :log_level (:info) - # Default log level to use - # - # @option args [Logger] :logger ($stdout) - # Logger to use for output + # The default log level to use with the Logger. # - # @option args [Integer] :max_attempts (3) - # An integer representing the maximum number of attempts that will be made for a single request, including the initial attempt. + # @option args [Logger] :logger (Logger.new($stdout, level: cfg.log_level)) + # The Logger instance to use for logging. # - # @option args [String] :retry_mode ('standard') - # Specifies which retry algorithm to use. Values are: - # * `standard` - A standardized set of retry rules across the AWS SDKs. This includes support for retry quotas, which limit the number of unsuccessful retries a client can make. - # * `adaptive` - An experimental retry mode that includes all the functionality of `standard` mode along with automatic client side throttling. This is a provisional mode that may change behavior in the future. + # @option args [Hearth::Retry::Strategy] :retry_strategy (Hearth::Retry::Standard.new) + # Specifies which retry strategy class to use. Strategy classes + # may have additional options, such as max_retries and backoff strategies. + # Available options are: + # * `Retry::Standard` - A standardized set of retry rules across the AWS SDKs. This includes support for retry quotas, which limit the number of unsuccessful retries a client can make. + # * `Retry::Adaptive` - An experimental retry mode that includes all the functionality of `standard` mode along with automatic client side throttling. This is a provisional mode that may change behavior in the future. # # @option args [Boolean] :stub_responses (false) # Enable response stubbing for testing. See {Hearth::ClientStubs stub_responses}. @@ -41,17 +37,14 @@ module HighScoreService # @option args [Boolean] :validate_input (true) # When `true`, request parameters are validated using the modeled shapes. # - # @!attribute adaptive_retry_wait_to_fill - # @return [Boolean] - # # @!attribute disable_host_prefix # @return [Boolean] # # @!attribute endpoint # @return [String] # - # @!attribute http_wire_trace - # @return [Boolean] + # @!attribute http_client + # @return [Hearth::HTTP::Client] # # @!attribute log_level # @return [Symbol] @@ -59,11 +52,8 @@ module HighScoreService # @!attribute logger # @return [Logger] # - # @!attribute max_attempts - # @return [Integer] - # - # @!attribute retry_mode - # @return [String] + # @!attribute retry_strategy + # @return [Hearth::Retry::Strategy] # # @!attribute stub_responses # @return [Boolean] @@ -72,14 +62,12 @@ module HighScoreService # @return [Boolean] # Config = ::Struct.new( - :adaptive_retry_wait_to_fill, :disable_host_prefix, :endpoint, - :http_wire_trace, + :http_client, :log_level, :logger, - :max_attempts, - :retry_mode, + :retry_strategy, :stub_responses, :validate_input, :interceptors, @@ -90,28 +78,24 @@ module HighScoreService private def validate! - Hearth::Validator.validate_types!(adaptive_retry_wait_to_fill, TrueClass, FalseClass, context: 'options[:adaptive_retry_wait_to_fill]') Hearth::Validator.validate_types!(disable_host_prefix, TrueClass, FalseClass, context: 'options[:disable_host_prefix]') Hearth::Validator.validate_types!(endpoint, String, context: 'options[:endpoint]') - Hearth::Validator.validate_types!(http_wire_trace, TrueClass, FalseClass, context: 'options[:http_wire_trace]') + Hearth::Validator.validate_types!(http_client, Hearth::HTTP::Client, context: 'options[:http_client]') Hearth::Validator.validate_types!(log_level, Symbol, context: 'options[:log_level]') Hearth::Validator.validate_types!(logger, Logger, context: 'options[:logger]') - Hearth::Validator.validate_types!(max_attempts, Integer, context: 'options[:max_attempts]') - Hearth::Validator.validate_types!(retry_mode, String, context: 'options[:retry_mode]') + Hearth::Validator.validate_types!(retry_strategy, Hearth::Retry::Strategy, context: 'options[:retry_strategy]') Hearth::Validator.validate_types!(stub_responses, TrueClass, FalseClass, context: 'options[:stub_responses]') Hearth::Validator.validate_types!(validate_input, TrueClass, FalseClass, context: 'options[:validate_input]') end def self.defaults @defaults ||= { - adaptive_retry_wait_to_fill: [true], disable_host_prefix: [false], endpoint: [proc { |cfg| cfg[:stub_responses] ? 'http://localhost' : nil } ], - http_wire_trace: [false], + http_client: [proc { |cfg| Hearth::HTTP::Client.new(logger: cfg[:logger]) }], log_level: [:info], logger: [proc { |cfg| Logger.new($stdout, level: cfg[:log_level]) } ], - max_attempts: [3], - retry_mode: ['standard'], + retry_strategy: [Hearth::Retry::Standard.new], stub_responses: [false], interceptors: [], validate_input: [true] diff --git a/codegen/projections/high_score_service/lib/high_score_service/params.rb b/codegen/projections/high_score_service/lib/high_score_service/params.rb index cfd5e5d8e..731defdfd 100644 --- a/codegen/projections/high_score_service/lib/high_score_service/params.rb +++ b/codegen/projections/high_score_service/lib/high_score_service/params.rb @@ -8,6 +8,7 @@ # WARNING ABOUT GENERATED CODE module HighScoreService + # @api private module Params module AttributeErrors diff --git a/codegen/projections/high_score_service/lib/high_score_service/parsers.rb b/codegen/projections/high_score_service/lib/high_score_service/parsers.rb index 03248fdad..7c8972146 100644 --- a/codegen/projections/high_score_service/lib/high_score_service/parsers.rb +++ b/codegen/projections/high_score_service/lib/high_score_service/parsers.rb @@ -8,6 +8,7 @@ # WARNING ABOUT GENERATED CODE module HighScoreService + # @api private module Parsers class AttributeErrors diff --git a/codegen/projections/high_score_service/lib/high_score_service/stubs.rb b/codegen/projections/high_score_service/lib/high_score_service/stubs.rb index a8d686613..457ef1771 100644 --- a/codegen/projections/high_score_service/lib/high_score_service/stubs.rb +++ b/codegen/projections/high_score_service/lib/high_score_service/stubs.rb @@ -8,6 +8,7 @@ # WARNING ABOUT GENERATED CODE module HighScoreService + # @api private module Stubs # Operation Stubber for CreateHighScore @@ -25,7 +26,7 @@ def self.stub(http_resp, stub:) http_resp.headers['Location'] = stub[:location] unless stub[:location].nil? || stub[:location].empty? http_resp.headers['Content-Type'] = 'application/json' data = Stubs::HighScoreAttributes.stub(stub[:high_score]) unless stub[:high_score].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -55,7 +56,7 @@ def self.stub(http_resp, stub:) http_resp.status = 200 http_resp.headers['Content-Type'] = 'application/json' data = Stubs::HighScoreAttributes.stub(stub[:high_score]) unless stub[:high_score].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -118,7 +119,7 @@ def self.stub(http_resp, stub:) http_resp.status = 200 http_resp.headers['Content-Type'] = 'application/json' data = Stubs::HighScores.stub(stub[:high_scores]) unless stub[:high_scores].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -135,7 +136,7 @@ def self.stub(http_resp, stub:) http_resp.status = 200 http_resp.headers['Content-Type'] = 'application/json' data = Stubs::HighScoreAttributes.stub(stub[:high_score]) unless stub[:high_score].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end end diff --git a/codegen/projections/high_score_service/lib/high_score_service/types.rb b/codegen/projections/high_score_service/lib/high_score_service/types.rb index 4a41bed57..fbe74bd5b 100644 --- a/codegen/projections/high_score_service/lib/high_score_service/types.rb +++ b/codegen/projections/high_score_service/lib/high_score_service/types.rb @@ -9,6 +9,7 @@ module HighScoreService module Types + # Input structure for CreateHighScore # # @!attribute high_score diff --git a/codegen/projections/high_score_service/lib/high_score_service/validators.rb b/codegen/projections/high_score_service/lib/high_score_service/validators.rb index 630c96453..2c63c915e 100644 --- a/codegen/projections/high_score_service/lib/high_score_service/validators.rb +++ b/codegen/projections/high_score_service/lib/high_score_service/validators.rb @@ -10,6 +10,7 @@ require 'time' module HighScoreService + # @api private module Validators class AttributeErrors diff --git a/codegen/projections/rails_json/lib/rails_json/builders.rb b/codegen/projections/rails_json/lib/rails_json/builders.rb index 6ca6bde5e..4e814bb50 100644 --- a/codegen/projections/rails_json/lib/rails_json/builders.rb +++ b/codegen/projections/rails_json/lib/rails_json/builders.rb @@ -10,6 +10,7 @@ require 'base64' module RailsJson + # @api private module Builders # Operation Builder for AllQueryStringTypes @@ -73,7 +74,7 @@ def self.build(http_req, input:) value.to_s unless value.nil? end end - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -99,7 +100,7 @@ def self.build(http_req, input:) params = Hearth::Query::ParamList.new params['baz'] = input[:baz].to_s unless input[:baz].nil? params['maybeSet'] = input[:maybe_set].to_s unless input[:maybe_set].nil? - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -119,7 +120,7 @@ def self.build(http_req, input:) ) ) params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -184,7 +185,7 @@ def self.build(http_req, input:) http_req.http_method = 'PUT' http_req.append_path('/DocumentType') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = {} @@ -200,7 +201,7 @@ def self.build(http_req, input:) http_req.http_method = 'PUT' http_req.append_path('/DocumentTypeAsPayload') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' http_req.body = StringIO.new(Hearth::JSON.dump(input[:document_value])) end @@ -223,7 +224,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/emptyoperation') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -241,7 +242,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/endpoint') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -251,7 +252,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/endpointwithhostlabel') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = {} @@ -293,6 +294,15 @@ def self.build(input) end end + # Structure Builder for GreetingStruct + class RenamedGreeting + def self.build(input) + data = {} + data[:salutation] = input[:salutation] unless input[:salutation].nil? + data + end + end + # Structure Builder for GreetingStruct class GreetingStruct def self.build(input) @@ -308,7 +318,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/greetingwitherrors') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -318,7 +328,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/HttpPayloadTraits') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/octet-stream' http_req.body = StringIO.new(input[:blob] || '') http_req.headers['X-Foo'] = input[:foo] unless input[:foo].nil? || input[:foo].empty? @@ -331,7 +341,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/HttpPayloadTraitsWithMediaType') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'text/plain' http_req.body = StringIO.new(input[:blob] || '') http_req.headers['X-Foo'] = input[:foo] unless input[:foo].nil? || input[:foo].empty? @@ -344,7 +354,7 @@ def self.build(http_req, input:) http_req.http_method = 'PUT' http_req.append_path('/HttpPayloadWithStructure') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = Builders::NestedPayload.build(input[:nested]) unless input[:nested].nil? http_req.body = StringIO.new(Hearth::JSON.dump(data)) @@ -357,9 +367,9 @@ def self.build(http_req, input:) http_req.http_method = 'GET' http_req.append_path('/HttpPrefixHeaders') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['X-Foo'] = input[:foo] unless input[:foo].nil? || input[:foo].empty? - input[:foo_map].each do |key, value| + input[:foo_map]&.each do |key, value| http_req.headers["X-Foo-#{key}"] = value unless value.nil? || value.empty? end end @@ -371,7 +381,7 @@ def self.build(http_req, input:) http_req.http_method = 'GET' http_req.append_path('/HttpPrefixHeadersResponse') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -392,7 +402,7 @@ def self.build(http_req, input:) ) ) params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -413,7 +423,7 @@ def self.build(http_req, input:) ) ) params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -458,7 +468,7 @@ def self.build(http_req, input:) ) ) params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -499,7 +509,7 @@ def self.build(http_req, input:) ) ) params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -509,7 +519,7 @@ def self.build(http_req, input:) http_req.http_method = 'PUT' http_req.append_path('/HttpResponseCode') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -519,7 +529,7 @@ def self.build(http_req, input:) http_req.http_method = 'GET' http_req.append_path('/IgnoreQueryParamsInResponse') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -529,7 +539,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/InputAndOutputWithHeaders') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['X-String'] = input[:header_string] unless input[:header_string].nil? || input[:header_string].empty? http_req.headers['X-Byte'] = input[:header_byte].to_s unless input[:header_byte].nil? http_req.headers['X-Short'] = input[:header_short].to_s unless input[:header_short].nil? @@ -539,43 +549,13 @@ def self.build(http_req, input:) http_req.headers['X-Double'] = Hearth::NumberHelper.serialize(input[:header_double]) unless input[:header_double].nil? http_req.headers['X-Boolean1'] = input[:header_true_bool].to_s unless input[:header_true_bool].nil? http_req.headers['X-Boolean2'] = input[:header_false_bool].to_s unless input[:header_false_bool].nil? - unless input[:header_string_list].nil? || input[:header_string_list].empty? - http_req.headers['X-StringList'] = input[:header_string_list] - .compact - .map { |s| (s.include?('"') || s.include?(",")) ? "\"#{s.gsub('"', '\"')}\"" : s } - .join(', ') - end - unless input[:header_string_set].nil? || input[:header_string_set].empty? - http_req.headers['X-StringSet'] = input[:header_string_set] - .compact - .map { |s| (s.include?('"') || s.include?(",")) ? "\"#{s.gsub('"', '\"')}\"" : s } - .join(', ') - end - unless input[:header_integer_list].nil? || input[:header_integer_list].empty? - http_req.headers['X-IntegerList'] = input[:header_integer_list] - .compact - .map { |s| s.to_s } - .join(', ') - end - unless input[:header_boolean_list].nil? || input[:header_boolean_list].empty? - http_req.headers['X-BooleanList'] = input[:header_boolean_list] - .compact - .map { |s| s.to_s } - .join(', ') - end - unless input[:header_timestamp_list].nil? || input[:header_timestamp_list].empty? - http_req.headers['X-TimestampList'] = input[:header_timestamp_list] - .compact - .map { |s| Hearth::TimeHelper.to_http_date(s) } - .join(', ') - end + http_req.headers['X-StringList'] = input[:header_string_list] unless input[:header_string_list].nil? || input[:header_string_list].empty? + http_req.headers['X-StringSet'] = input[:header_string_set] unless input[:header_string_set].nil? || input[:header_string_set].empty? + http_req.headers['X-IntegerList'] = input[:header_integer_list] unless input[:header_integer_list].nil? || input[:header_integer_list].empty? + http_req.headers['X-BooleanList'] = input[:header_boolean_list] unless input[:header_boolean_list].nil? || input[:header_boolean_list].empty? + http_req.headers['X-TimestampList'] = input[:header_timestamp_list] unless input[:header_timestamp_list].nil? || input[:header_timestamp_list].empty? http_req.headers['X-Enum'] = input[:header_enum] unless input[:header_enum].nil? || input[:header_enum].empty? - unless input[:header_enum_list].nil? || input[:header_enum_list].empty? - http_req.headers['X-EnumList'] = input[:header_enum_list] - .compact - .map { |s| (s.include?('"') || s.include?(",")) ? "\"#{s.gsub('"', '\"')}\"" : s } - .join(', ') - end + http_req.headers['X-EnumList'] = input[:header_enum_list] unless input[:header_enum_list].nil? || input[:header_enum_list].empty? end end @@ -607,7 +587,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/jsonenums') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = {} @@ -627,7 +607,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/JsonMaps') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = {} @@ -651,7 +631,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/jsonunions') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = {} @@ -700,7 +680,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = {} @@ -850,7 +830,7 @@ def self.build(http_req, input:) http_req.http_method = 'GET' http_req.append_path('/MediaTypeHeader') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['X-Json'] = ::Base64::encode64(input[:json]).strip unless input[:json].nil? || input[:json].empty? end end @@ -878,6 +858,8 @@ def self.build(input) data[:map_value] = (Builders::StringMap.build(input) unless input.nil?) when Types::MyUnion::StructureValue data[:structure_value] = (Builders::GreetingStruct.build(input) unless input.nil?) + when Types::MyUnion::RenamedStructureValue + data[:renamed_structure_value] = (Builders::RenamedGreeting.build(input) unless input.nil?) else raise ArgumentError, "Expected input to be one of the subclasses of Types::MyUnion" @@ -893,7 +875,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/nestedattributes') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = {} @@ -918,15 +900,10 @@ def self.build(http_req, input:) http_req.http_method = 'GET' http_req.append_path('/NullAndEmptyHeadersClient') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['X-A'] = input[:a] unless input[:a].nil? || input[:a].empty? http_req.headers['X-B'] = input[:b] unless input[:b].nil? || input[:b].empty? - unless input[:c].nil? || input[:c].empty? - http_req.headers['X-C'] = input[:c] - .compact - .map { |s| (s.include?('"') || s.include?(",")) ? "\"#{s.gsub('"', '\"')}\"" : s } - .join(', ') - end + http_req.headers['X-C'] = input[:c] unless input[:c].nil? || input[:c].empty? end end @@ -936,7 +913,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/nulloperation') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = {} @@ -955,7 +932,7 @@ def self.build(http_req, input:) params = Hearth::Query::ParamList.new params['Null'] = input[:null_value].to_s unless input[:null_value].nil? params['Empty'] = input[:empty_string].to_s unless input[:empty_string].nil? - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -965,7 +942,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/operationwithoptionalinputoutput') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = {} @@ -981,7 +958,7 @@ def self.build(http_req, input:) http_req.append_path('/QueryIdempotencyTokenAutoFill') params = Hearth::Query::ParamList.new params['token'] = input[:token].to_s unless input[:token].nil? - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -1001,7 +978,7 @@ def self.build(http_req, input:) end end params['corge'] = input[:qux].to_s unless input[:qux].nil? - http_req.append_query_params(params) + http_req.append_query_param_list(params) end end @@ -1086,7 +1063,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/streamingoperation') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.body = input[:output] http_req.headers['Transfer-Encoding'] = 'chunked' http_req.headers['Content-Type'] = 'application/octet-stream' @@ -1152,7 +1129,7 @@ def self.build(http_req, input:) http_req.http_method = 'POST' http_req.append_path('/TimestampFormatHeaders') params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['X-memberEpochSeconds'] = Hearth::TimeHelper.to_epoch_seconds(input[:member_epoch_seconds]).to_i unless input[:member_epoch_seconds].nil? http_req.headers['X-memberHttpDate'] = Hearth::TimeHelper.to_http_date(input[:member_http_date]) unless input[:member_http_date].nil? http_req.headers['X-memberDateTime'] = Hearth::TimeHelper.to_date_time(input[:member_date_time]) unless input[:member_date_time].nil? @@ -1196,7 +1173,7 @@ def self.build(http_req, input:) ) ) params = Hearth::Query::ParamList.new - http_req.append_query_params(params) + http_req.append_query_param_list(params) http_req.headers['Content-Type'] = 'application/json' data = {} diff --git a/codegen/projections/rails_json/lib/rails_json/client.rb b/codegen/projections/rails_json/lib/rails_json/client.rb index 8e846344f..31351e254 100644 --- a/codegen/projections/rails_json/lib/rails_json/client.rb +++ b/codegen/projections/rails_json/lib/rails_json/client.rb @@ -31,8 +31,6 @@ def initialize(config = RailsJson::Config.new, options = {}) @config = config @middleware = Hearth::MiddlewareBuilder.new(options[:middleware]) @stubs = Hearth::Stubbing::Stubs.new - @retry_quota = Hearth::Retry::RetryQuota.new - @client_rate_limiter = Hearth::Retry::ClientRateLimiter.new end # This example uses all query string types. @@ -101,12 +99,8 @@ def all_query_string_types(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -115,7 +109,7 @@ def all_query_string_types(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::AllQueryStringTypes, stubs: @stubs, params_class: Params::AllQueryStringTypesOutput @@ -125,7 +119,7 @@ def all_query_string_types(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -169,12 +163,8 @@ def constant_and_variable_query_string(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -183,7 +173,7 @@ def constant_and_variable_query_string(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::ConstantAndVariableQueryString, stubs: @stubs, params_class: Params::ConstantAndVariableQueryStringOutput @@ -193,7 +183,7 @@ def constant_and_variable_query_string(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -237,12 +227,8 @@ def constant_query_string(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -251,7 +237,7 @@ def constant_query_string(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::ConstantQueryString, stubs: @stubs, params_class: Params::ConstantQueryStringOutput @@ -261,7 +247,7 @@ def constant_query_string(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -312,12 +298,8 @@ def document_type(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -326,7 +308,7 @@ def document_type(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::DocumentType, stubs: @stubs, params_class: Params::DocumentTypeOutput @@ -336,7 +318,7 @@ def document_type(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -385,12 +367,8 @@ def document_type_as_payload(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -399,7 +377,7 @@ def document_type_as_payload(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::DocumentTypeAsPayload, stubs: @stubs, params_class: Params::DocumentTypeAsPayloadOutput @@ -409,7 +387,7 @@ def document_type_as_payload(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -446,12 +424,8 @@ def empty_operation(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -460,7 +434,7 @@ def empty_operation(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::EmptyOperation, stubs: @stubs, params_class: Params::EmptyOperationOutput @@ -470,7 +444,7 @@ def empty_operation(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -511,12 +485,8 @@ def endpoint_operation(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -525,7 +495,7 @@ def endpoint_operation(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::EndpointOperation, stubs: @stubs, params_class: Params::EndpointOperationOutput @@ -535,7 +505,7 @@ def endpoint_operation(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -578,12 +548,8 @@ def endpoint_with_host_label_operation(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -592,7 +558,7 @@ def endpoint_with_host_label_operation(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::EndpointWithHostLabelOperation, stubs: @stubs, params_class: Params::EndpointWithHostLabelOperationOutput @@ -602,7 +568,7 @@ def endpoint_with_host_label_operation(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -649,12 +615,8 @@ def greeting_with_errors(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: [Errors::InvalidGreeting, Errors::ComplexError]), @@ -663,7 +625,7 @@ def greeting_with_errors(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::GreetingWithErrors, stubs: @stubs, params_class: Params::GreetingWithErrorsOutput @@ -673,7 +635,7 @@ def greeting_with_errors(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -720,12 +682,8 @@ def http_payload_traits(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -734,7 +692,7 @@ def http_payload_traits(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::HttpPayloadTraits, stubs: @stubs, params_class: Params::HttpPayloadTraitsOutput @@ -744,7 +702,7 @@ def http_payload_traits(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -789,12 +747,8 @@ def http_payload_traits_with_media_type(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -803,7 +757,7 @@ def http_payload_traits_with_media_type(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::HttpPayloadTraitsWithMediaType, stubs: @stubs, params_class: Params::HttpPayloadTraitsWithMediaTypeOutput @@ -813,7 +767,7 @@ def http_payload_traits_with_media_type(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -863,12 +817,8 @@ def http_payload_with_structure(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -877,7 +827,7 @@ def http_payload_with_structure(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::HttpPayloadWithStructure, stubs: @stubs, params_class: Params::HttpPayloadWithStructureOutput @@ -887,7 +837,7 @@ def http_payload_with_structure(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -936,12 +886,8 @@ def http_prefix_headers(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -950,7 +896,7 @@ def http_prefix_headers(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::HttpPrefixHeaders, stubs: @stubs, params_class: Params::HttpPrefixHeadersOutput @@ -960,7 +906,7 @@ def http_prefix_headers(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -1001,12 +947,8 @@ def http_prefix_headers_in_response(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1015,7 +957,7 @@ def http_prefix_headers_in_response(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::HttpPrefixHeadersInResponse, stubs: @stubs, params_class: Params::HttpPrefixHeadersInResponseOutput @@ -1025,7 +967,7 @@ def http_prefix_headers_in_response(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -1065,12 +1007,8 @@ def http_request_with_float_labels(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1079,7 +1017,7 @@ def http_request_with_float_labels(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::HttpRequestWithFloatLabels, stubs: @stubs, params_class: Params::HttpRequestWithFloatLabelsOutput @@ -1089,7 +1027,7 @@ def http_request_with_float_labels(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -1129,12 +1067,8 @@ def http_request_with_greedy_label_in_path(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1143,7 +1077,7 @@ def http_request_with_greedy_label_in_path(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::HttpRequestWithGreedyLabelInPath, stubs: @stubs, params_class: Params::HttpRequestWithGreedyLabelInPathOutput @@ -1153,7 +1087,7 @@ def http_request_with_greedy_label_in_path(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -1208,12 +1142,8 @@ def http_request_with_labels(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1222,7 +1152,7 @@ def http_request_with_labels(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::HttpRequestWithLabels, stubs: @stubs, params_class: Params::HttpRequestWithLabelsOutput @@ -1232,7 +1162,7 @@ def http_request_with_labels(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -1280,12 +1210,8 @@ def http_request_with_labels_and_timestamp_format(params = {}, options = {}, &bl ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1294,7 +1220,7 @@ def http_request_with_labels_and_timestamp_format(params = {}, options = {}, &bl stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::HttpRequestWithLabelsAndTimestampFormat, stubs: @stubs, params_class: Params::HttpRequestWithLabelsAndTimestampFormatOutput @@ -1304,7 +1230,7 @@ def http_request_with_labels_and_timestamp_format(params = {}, options = {}, &bl resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -1342,12 +1268,8 @@ def http_response_code(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1356,7 +1278,7 @@ def http_response_code(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::HttpResponseCode, stubs: @stubs, params_class: Params::HttpResponseCodeOutput @@ -1366,7 +1288,7 @@ def http_response_code(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -1408,12 +1330,8 @@ def ignore_query_params_in_response(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1422,7 +1340,7 @@ def ignore_query_params_in_response(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::IgnoreQueryParamsInResponse, stubs: @stubs, params_class: Params::IgnoreQueryParamsInResponseOutput @@ -1432,7 +1350,7 @@ def ignore_query_params_in_response(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -1523,12 +1441,8 @@ def input_and_output_with_headers(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1537,7 +1451,7 @@ def input_and_output_with_headers(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::InputAndOutputWithHeaders, stubs: @stubs, params_class: Params::InputAndOutputWithHeadersOutput @@ -1547,7 +1461,7 @@ def input_and_output_with_headers(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -1608,12 +1522,8 @@ def json_enums(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1622,7 +1532,7 @@ def json_enums(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::JsonEnums, stubs: @stubs, params_class: Params::JsonEnumsOutput @@ -1632,7 +1542,7 @@ def json_enums(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -1720,12 +1630,8 @@ def json_maps(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1734,7 +1640,7 @@ def json_maps(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::JsonMaps, stubs: @stubs, params_class: Params::JsonMapsOutput @@ -1744,7 +1650,7 @@ def json_maps(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -1784,6 +1690,9 @@ def json_maps(params = {}, options = {}, &block) # }, # structure_value: { # hi: 'hi' + # }, + # renamed_structure_value: { + # salutation: 'salutation' # } # } # ) @@ -1791,7 +1700,7 @@ def json_maps(params = {}, options = {}, &block) # @example Response structure # # resp.data #=> Types::JsonUnionsOutput - # resp.data.contents #=> Types::MyUnion, one of [StringValue, BooleanValue, NumberValue, BlobValue, TimestampValue, EnumValue, ListValue, MapValue, StructureValue] + # resp.data.contents #=> Types::MyUnion, one of [StringValue, BooleanValue, NumberValue, BlobValue, TimestampValue, EnumValue, ListValue, MapValue, StructureValue, RenamedStructureValue] # resp.data.contents.string_value #=> String # resp.data.contents.boolean_value #=> Boolean # resp.data.contents.number_value #=> Integer @@ -1804,6 +1713,8 @@ def json_maps(params = {}, options = {}, &block) # resp.data.contents.map_value['key'] #=> String # resp.data.contents.structure_value #=> Types::GreetingStruct # resp.data.contents.structure_value.hi #=> String + # resp.data.contents.renamed_structure_value #=> Types::RenamedGreeting + # resp.data.contents.renamed_structure_value.salutation #=> String # def json_unions(params = {}, options = {}, &block) stack = Hearth::MiddlewareStack.new @@ -1818,12 +1729,8 @@ def json_unions(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -1832,7 +1739,7 @@ def json_unions(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::JsonUnions, stubs: @stubs, params_class: Params::JsonUnionsOutput @@ -1842,7 +1749,7 @@ def json_unions(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -1987,12 +1894,8 @@ def kitchen_sink_operation(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: [Errors::ErrorWithMembers, Errors::ErrorWithoutMembers]), @@ -2001,7 +1904,7 @@ def kitchen_sink_operation(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::KitchenSinkOperation, stubs: @stubs, params_class: Params::KitchenSinkOperationOutput @@ -2011,7 +1914,7 @@ def kitchen_sink_operation(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -2053,12 +1956,8 @@ def media_type_header(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2067,7 +1966,7 @@ def media_type_header(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::MediaTypeHeader, stubs: @stubs, params_class: Params::MediaTypeHeaderOutput @@ -2077,7 +1976,7 @@ def media_type_header(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -2119,12 +2018,8 @@ def nested_attributes_operation(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2133,7 +2028,7 @@ def nested_attributes_operation(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::NestedAttributesOperation, stubs: @stubs, params_class: Params::NestedAttributesOperationOutput @@ -2143,7 +2038,7 @@ def nested_attributes_operation(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -2194,12 +2089,8 @@ def null_and_empty_headers_client(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2208,7 +2099,7 @@ def null_and_empty_headers_client(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::NullAndEmptyHeadersClient, stubs: @stubs, params_class: Params::NullAndEmptyHeadersClientOutput @@ -2218,7 +2109,7 @@ def null_and_empty_headers_client(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -2268,12 +2159,8 @@ def null_operation(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2282,7 +2169,7 @@ def null_operation(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::NullOperation, stubs: @stubs, params_class: Params::NullOperationOutput @@ -2292,7 +2179,7 @@ def null_operation(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -2334,12 +2221,8 @@ def omits_null_serializes_empty_string(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2348,7 +2231,7 @@ def omits_null_serializes_empty_string(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::OmitsNullSerializesEmptyString, stubs: @stubs, params_class: Params::OmitsNullSerializesEmptyStringOutput @@ -2358,7 +2241,7 @@ def omits_null_serializes_empty_string(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -2398,12 +2281,8 @@ def operation_with_optional_input_output(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2412,7 +2291,7 @@ def operation_with_optional_input_output(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::OperationWithOptionalInputOutput, stubs: @stubs, params_class: Params::OperationWithOptionalInputOutputOutput @@ -2422,7 +2301,7 @@ def operation_with_optional_input_output(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -2465,12 +2344,8 @@ def query_idempotency_token_auto_fill(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2479,7 +2354,7 @@ def query_idempotency_token_auto_fill(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::QueryIdempotencyTokenAutoFill, stubs: @stubs, params_class: Params::QueryIdempotencyTokenAutoFillOutput @@ -2489,7 +2364,7 @@ def query_idempotency_token_auto_fill(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -2533,12 +2408,8 @@ def query_params_as_string_list_map(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2547,7 +2418,7 @@ def query_params_as_string_list_map(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::QueryParamsAsStringListMap, stubs: @stubs, params_class: Params::QueryParamsAsStringListMapOutput @@ -2557,7 +2428,7 @@ def query_params_as_string_list_map(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -2596,12 +2467,8 @@ def streaming_operation(params = {}, options = {}, &block) builder: Builders::StreamingOperation ) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2610,7 +2477,7 @@ def streaming_operation(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::StreamingOperation, stubs: @stubs, params_class: Params::StreamingOperationOutput @@ -2620,7 +2487,7 @@ def streaming_operation(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -2674,12 +2541,8 @@ def timestamp_format_headers(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2688,7 +2551,7 @@ def timestamp_format_headers(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::TimestampFormatHeaders, stubs: @stubs, params_class: Params::TimestampFormatHeadersOutput @@ -2698,7 +2561,7 @@ def timestamp_format_headers(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -2742,12 +2605,8 @@ def operation____789_bad_name(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -2756,7 +2615,7 @@ def operation____789_bad_name(params = {}, options = {}, &block) stack.use(Middleware::RequestId) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::Operation____789BadName, stubs: @stubs, params_class: Params::Struct____789BadNameOutput @@ -2766,7 +2625,7 @@ def operation____789_bad_name(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, diff --git a/codegen/projections/rails_json/lib/rails_json/config.rb b/codegen/projections/rails_json/lib/rails_json/config.rb index 12645d4a5..bd0b1e4ac 100644 --- a/codegen/projections/rails_json/lib/rails_json/config.rb +++ b/codegen/projections/rails_json/lib/rails_json/config.rb @@ -9,31 +9,27 @@ module RailsJson # @!method initialize(*options) - # @option args [Boolean] :adaptive_retry_wait_to_fill (true) - # Used only in `adaptive` retry mode. When true, the request will sleep until there is sufficient client side capacity to retry the request. When false, the request will raise a `CapacityNotAvailableError` and will not retry instead of sleeping. - # # @option args [Boolean] :disable_host_prefix (false) # When `true`, does not perform host prefix injection using @endpoint's hostPrefix property. # # @option args [String] :endpoint # Endpoint of the service # - # @option args [Boolean] :http_wire_trace (false) - # Enable debug wire trace on http requests. + # @option args [Hearth::HTTP::Client] :http_client (Hearth::HTTP::Client.new) + # The HTTP Client to use for request transport. # # @option args [Symbol] :log_level (:info) - # Default log level to use - # - # @option args [Logger] :logger ($stdout) - # Logger to use for output + # The default log level to use with the Logger. # - # @option args [Integer] :max_attempts (3) - # An integer representing the maximum number of attempts that will be made for a single request, including the initial attempt. + # @option args [Logger] :logger (Logger.new($stdout, level: cfg.log_level)) + # The Logger instance to use for logging. # - # @option args [String] :retry_mode ('standard') - # Specifies which retry algorithm to use. Values are: - # * `standard` - A standardized set of retry rules across the AWS SDKs. This includes support for retry quotas, which limit the number of unsuccessful retries a client can make. - # * `adaptive` - An experimental retry mode that includes all the functionality of `standard` mode along with automatic client side throttling. This is a provisional mode that may change behavior in the future. + # @option args [Hearth::Retry::Strategy] :retry_strategy (Hearth::Retry::Standard.new) + # Specifies which retry strategy class to use. Strategy classes + # may have additional options, such as max_retries and backoff strategies. + # Available options are: + # * `Retry::Standard` - A standardized set of retry rules across the AWS SDKs. This includes support for retry quotas, which limit the number of unsuccessful retries a client can make. + # * `Retry::Adaptive` - An experimental retry mode that includes all the functionality of `standard` mode along with automatic client side throttling. This is a provisional mode that may change behavior in the future. # # @option args [Boolean] :stub_responses (false) # Enable response stubbing for testing. See {Hearth::ClientStubs stub_responses}. @@ -41,17 +37,14 @@ module RailsJson # @option args [Boolean] :validate_input (true) # When `true`, request parameters are validated using the modeled shapes. # - # @!attribute adaptive_retry_wait_to_fill - # @return [Boolean] - # # @!attribute disable_host_prefix # @return [Boolean] # # @!attribute endpoint # @return [String] # - # @!attribute http_wire_trace - # @return [Boolean] + # @!attribute http_client + # @return [Hearth::HTTP::Client] # # @!attribute log_level # @return [Symbol] @@ -59,11 +52,8 @@ module RailsJson # @!attribute logger # @return [Logger] # - # @!attribute max_attempts - # @return [Integer] - # - # @!attribute retry_mode - # @return [String] + # @!attribute retry_strategy + # @return [Hearth::Retry::Strategy] # # @!attribute stub_responses # @return [Boolean] @@ -72,14 +62,12 @@ module RailsJson # @return [Boolean] # Config = ::Struct.new( - :adaptive_retry_wait_to_fill, :disable_host_prefix, :endpoint, - :http_wire_trace, + :http_client, :log_level, :logger, - :max_attempts, - :retry_mode, + :retry_strategy, :stub_responses, :validate_input, keyword_init: true @@ -89,28 +77,24 @@ module RailsJson private def validate! - Hearth::Validator.validate_types!(adaptive_retry_wait_to_fill, TrueClass, FalseClass, context: 'options[:adaptive_retry_wait_to_fill]') Hearth::Validator.validate_types!(disable_host_prefix, TrueClass, FalseClass, context: 'options[:disable_host_prefix]') Hearth::Validator.validate_types!(endpoint, String, context: 'options[:endpoint]') - Hearth::Validator.validate_types!(http_wire_trace, TrueClass, FalseClass, context: 'options[:http_wire_trace]') + Hearth::Validator.validate_types!(http_client, Hearth::HTTP::Client, context: 'options[:http_client]') Hearth::Validator.validate_types!(log_level, Symbol, context: 'options[:log_level]') Hearth::Validator.validate_types!(logger, Logger, context: 'options[:logger]') - Hearth::Validator.validate_types!(max_attempts, Integer, context: 'options[:max_attempts]') - Hearth::Validator.validate_types!(retry_mode, String, context: 'options[:retry_mode]') + Hearth::Validator.validate_types!(retry_strategy, Hearth::Retry::Strategy, context: 'options[:retry_strategy]') Hearth::Validator.validate_types!(stub_responses, TrueClass, FalseClass, context: 'options[:stub_responses]') Hearth::Validator.validate_types!(validate_input, TrueClass, FalseClass, context: 'options[:validate_input]') end def self.defaults @defaults ||= { - adaptive_retry_wait_to_fill: [true], disable_host_prefix: [false], endpoint: [proc { |cfg| cfg[:stub_responses] ? 'http://localhost' : nil } ], - http_wire_trace: [false], + http_client: [proc { |cfg| Hearth::HTTP::Client.new(logger: cfg[:logger]) }], log_level: [:info], logger: [proc { |cfg| Logger.new($stdout, level: cfg[:log_level]) } ], - max_attempts: [3], - retry_mode: ['standard'], + retry_strategy: [Hearth::Retry::Standard.new], stub_responses: [false], validate_input: [true] }.freeze diff --git a/codegen/projections/rails_json/lib/rails_json/params.rb b/codegen/projections/rails_json/lib/rails_json/params.rb index d54e5da54..879fbb93a 100644 --- a/codegen/projections/rails_json/lib/rails_json/params.rb +++ b/codegen/projections/rails_json/lib/rails_json/params.rb @@ -10,6 +10,7 @@ require 'securerandom' module RailsJson + # @api private module Params module AllQueryStringTypesInput @@ -338,6 +339,15 @@ def self.build(params, context: '') end end + module RenamedGreeting + def self.build(params, context: '') + Hearth::Validator.validate_types!(params, ::Hash, Types::RenamedGreeting, context: context) + type = Types::RenamedGreeting.new + type.salutation = params[:salutation] + type + end + end + module GreetingWithErrorsInput def self.build(params, context: '') Hearth::Validator.validate_types!(params, ::Hash, Types::GreetingWithErrorsInput, context: context) @@ -1004,9 +1014,13 @@ def self.build(params, context: '') Types::MyUnion::StructureValue.new( (GreetingStruct.build(params[:structure_value], context: "#{context}[:structure_value]") unless params[:structure_value].nil?) ) + when :renamed_structure_value + Types::MyUnion::RenamedStructureValue.new( + (RenamedGreeting.build(params[:renamed_structure_value], context: "#{context}[:renamed_structure_value]") unless params[:renamed_structure_value].nil?) + ) else raise ArgumentError, - "Expected #{context} to have one of :string_value, :boolean_value, :number_value, :blob_value, :timestamp_value, :enum_value, :list_value, :map_value, :structure_value set" + "Expected #{context} to have one of :string_value, :boolean_value, :number_value, :blob_value, :timestamp_value, :enum_value, :list_value, :map_value, :structure_value, :renamed_structure_value set" end end end diff --git a/codegen/projections/rails_json/lib/rails_json/parsers.rb b/codegen/projections/rails_json/lib/rails_json/parsers.rb index d2f3a2ec3..001c03a81 100644 --- a/codegen/projections/rails_json/lib/rails_json/parsers.rb +++ b/codegen/projections/rails_json/lib/rails_json/parsers.rb @@ -10,6 +10,7 @@ require 'base64' module RailsJson + # @api private module Parsers # Operation Parser for AllQueryStringTypes @@ -222,6 +223,14 @@ def self.parse(list) end end + class RenamedGreeting + def self.parse(map) + data = Types::RenamedGreeting.new + data.salutation = map['salutation'] + return data + end + end + class GreetingStruct def self.parse(map) data = Types::GreetingStruct.new @@ -668,6 +677,9 @@ def self.parse(map) when 'structure_value' value = (Parsers::GreetingStruct.parse(value) unless value.nil?) Types::MyUnion::StructureValue.new(value) if value + when 'renamed_structure_value' + value = (Parsers::RenamedGreeting.parse(value) unless value.nil?) + Types::MyUnion::RenamedStructureValue.new(value) if value else Types::MyUnion::Unknown.new({name: key, value: value}) end diff --git a/codegen/projections/rails_json/lib/rails_json/stubs.rb b/codegen/projections/rails_json/lib/rails_json/stubs.rb index 5714ec5e8..35d7f4c19 100644 --- a/codegen/projections/rails_json/lib/rails_json/stubs.rb +++ b/codegen/projections/rails_json/lib/rails_json/stubs.rb @@ -10,6 +10,7 @@ require 'base64' module RailsJson + # @api private module Stubs # Operation Stubber for AllQueryStringTypes @@ -199,7 +200,7 @@ def self.stub(http_resp, stub:) http_resp.headers['Content-Type'] = 'application/json' data[:string_value] = stub[:string_value] unless stub[:string_value].nil? data[:document_value] = stub[:document_value] unless stub[:document_value].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -215,7 +216,7 @@ def self.stub(http_resp, stub:) data = {} http_resp.status = 200 http_resp.headers['Content-Type'] = 'application/json' - http_resp.body = StringIO.new(Hearth::JSON.dump(stub[:document_value])) + http_resp.body.write(Hearth::JSON.dump(stub[:document_value])) end end @@ -334,6 +335,24 @@ def self.stub(stub) end end + # Structure Stubber for GreetingStruct + class RenamedGreeting + def self.default(visited=[]) + return nil if visited.include?('RenamedGreeting') + visited = visited + ['RenamedGreeting'] + { + salutation: 'salutation', + } + end + + def self.stub(stub) + stub ||= Types::RenamedGreeting.new + data = {} + data[:salutation] = stub[:salutation] unless stub[:salutation].nil? + data + end + end + # Structure Stubber for GreetingStruct class GreetingStruct def self.default(visited=[]) @@ -365,7 +384,7 @@ def self.stub(http_resp, stub:) http_resp.status = 200 http_resp.headers['Content-Type'] = 'application/json' data[:greeting] = stub[:greeting] unless stub[:greeting].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -383,7 +402,7 @@ def self.stub(http_resp, stub:) http_resp.status = 200 http_resp.headers['X-Foo'] = stub[:foo] unless stub[:foo].nil? || stub[:foo].empty? http_resp.headers['Content-Type'] = 'application/octet-stream' - http_resp.body = StringIO.new(stub[:blob] || '') + http_resp.body.write(stub[:blob] || '') end end @@ -401,7 +420,7 @@ def self.stub(http_resp, stub:) http_resp.status = 200 http_resp.headers['X-Foo'] = stub[:foo] unless stub[:foo].nil? || stub[:foo].empty? http_resp.headers['Content-Type'] = 'text/plain' - http_resp.body = StringIO.new(stub[:blob] || '') + http_resp.body.write(stub[:blob] || '') end end @@ -418,7 +437,7 @@ def self.stub(http_resp, stub:) http_resp.status = 200 http_resp.headers['Content-Type'] = 'application/json' data = Stubs::NestedPayload.stub(stub[:nested]) unless stub[:nested].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -657,7 +676,7 @@ def self.stub(http_resp, stub:) data[:foo_enum_list] = Stubs::FooEnumList.stub(stub[:foo_enum_list]) unless stub[:foo_enum_list].nil? data[:foo_enum_set] = Stubs::FooEnumSet.stub(stub[:foo_enum_set]) unless stub[:foo_enum_set].nil? data[:foo_enum_map] = Stubs::FooEnumMap.stub(stub[:foo_enum_map]) unless stub[:foo_enum_map].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -692,7 +711,7 @@ def self.stub(http_resp, stub:) data[:sparse_string_map] = Stubs::SparseStringMap.stub(stub[:sparse_string_map]) unless stub[:sparse_string_map].nil? data[:dense_set_map] = Stubs::DenseSetMap.stub(stub[:dense_set_map]) unless stub[:dense_set_map].nil? data[:sparse_set_map] = Stubs::SparseSetMap.stub(stub[:sparse_set_map]) unless stub[:sparse_set_map].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -709,7 +728,7 @@ def self.stub(http_resp, stub:) http_resp.status = 200 http_resp.headers['Content-Type'] = 'application/json' data[:contents] = Stubs::MyUnion.stub(stub[:contents]) unless stub[:contents].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -844,7 +863,7 @@ def self.stub(http_resp, stub:) data[:struct_with_location_name] = Stubs::StructWithLocationName.stub(stub[:struct_with_location_name]) unless stub[:struct_with_location_name].nil? data[:timestamp] = Hearth::TimeHelper.to_date_time(stub[:timestamp]) unless stub[:timestamp].nil? data[:unix_timestamp] = Hearth::TimeHelper.to_epoch_seconds(stub[:unix_timestamp]).to_i unless stub[:unix_timestamp].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -1094,6 +1113,8 @@ def self.stub(stub) data[:map_value] = (Stubs::StringMap.stub(stub.__getobj__) unless stub.__getobj__.nil?) when Types::MyUnion::StructureValue data[:structure_value] = (Stubs::GreetingStruct.stub(stub.__getobj__) unless stub.__getobj__.nil?) + when Types::MyUnion::RenamedStructureValue + data[:renamed_structure_value] = (Stubs::RenamedGreeting.stub(stub.__getobj__) unless stub.__getobj__.nil?) else raise ArgumentError, "Expected input to be one of the subclasses of Types::MyUnion" @@ -1116,7 +1137,7 @@ def self.stub(http_resp, stub:) http_resp.status = 200 http_resp.headers['Content-Type'] = 'application/json' data[:value] = stub[:value] unless stub[:value].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -1181,7 +1202,7 @@ def self.stub(http_resp, stub:) data[:string] = stub[:string] unless stub[:string].nil? data[:sparse_string_list] = Stubs::SparseStringList.stub(stub[:sparse_string_list]) unless stub[:sparse_string_list].nil? data[:sparse_string_map] = Stubs::SparseStringMap.stub(stub[:sparse_string_map]) unless stub[:sparse_string_map].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -1211,7 +1232,7 @@ def self.stub(http_resp, stub:) http_resp.status = 200 http_resp.headers['Content-Type'] = 'application/json' data[:value] = stub[:value] unless stub[:value].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end @@ -1550,7 +1571,7 @@ def self.stub(http_resp, stub:) http_resp.status = 200 http_resp.headers['Content-Type'] = 'application/json' data[:member] = Stubs::Struct____456efg.stub(stub[:member]) unless stub[:member].nil? - http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + http_resp.body.write(Hearth::JSON.dump(data)) end end end diff --git a/codegen/projections/rails_json/lib/rails_json/types.rb b/codegen/projections/rails_json/lib/rails_json/types.rb index 421cff161..a05b0e78f 100644 --- a/codegen/projections/rails_json/lib/rails_json/types.rb +++ b/codegen/projections/rails_json/lib/rails_json/types.rb @@ -1650,6 +1650,16 @@ def to_s end end + class RenamedStructureValue < MyUnion + def to_h + { renamed_structure_value: super(__getobj__) } + end + + def to_s + "#" + end + end + # Handles unknown future members # class Unknown < MyUnion @@ -1871,6 +1881,17 @@ def to_s include Hearth::Structure end + # @!attribute salutation + # + # @return [String] + # + RenamedGreeting = ::Struct.new( + :salutation, + keyword_init: true + ) do + include Hearth::Structure + end + # @!attribute value # # @return [String] diff --git a/codegen/projections/rails_json/lib/rails_json/validators.rb b/codegen/projections/rails_json/lib/rails_json/validators.rb index 93f5610cd..f61b596f8 100644 --- a/codegen/projections/rails_json/lib/rails_json/validators.rb +++ b/codegen/projections/rails_json/lib/rails_json/validators.rb @@ -10,6 +10,7 @@ require 'time' module RailsJson + # @api private module Validators class AllQueryStringTypesInput @@ -298,6 +299,13 @@ def self.validate!(input, context:) end end + class RenamedGreeting + def self.validate!(input, context:) + Hearth::Validator.validate_types!(input, Types::RenamedGreeting, context: context) + Hearth::Validator.validate_types!(input[:salutation], ::String, context: "#{context}[:salutation]") + end + end + class GreetingWithErrorsInput def self.validate!(input, context:) Hearth::Validator.validate_types!(input, Types::GreetingWithErrorsInput, context: context) @@ -863,6 +871,8 @@ def self.validate!(input, context:) StringMap.validate!(input.__getobj__, context: context) unless input.__getobj__.nil? when Types::MyUnion::StructureValue GreetingStruct.validate!(input.__getobj__, context: context) unless input.__getobj__.nil? + when Types::MyUnion::RenamedStructureValue + RenamedGreeting.validate!(input.__getobj__, context: context) unless input.__getobj__.nil? else raise ArgumentError, "Expected #{context} to be a union member of "\ @@ -923,6 +933,12 @@ def self.validate!(input, context:) Validators::GreetingStruct.validate!(input, context: context) unless input.nil? end end + + class RenamedStructureValue + def self.validate!(input, context:) + Validators::RenamedGreeting.validate!(input, context: context) unless input.nil? + end + end end class NestedAttributesOperationInput diff --git a/codegen/projections/rails_json/sig/rails_json/types.rbs b/codegen/projections/rails_json/sig/rails_json/types.rbs index 3118283a3..db92aa455 100644 --- a/codegen/projections/rails_json/sig/rails_json/types.rbs +++ b/codegen/projections/rails_json/sig/rails_json/types.rbs @@ -179,6 +179,10 @@ module RailsJson def to_h: () -> { structure_value: Hash[Symbol,MyUnion] } end + class RenamedStructureValue < MyUnion + def to_h: () -> { renamed_structure_value: Hash[Symbol,MyUnion] } + end + class Unknown < MyUnion def to_h: () -> { unknown: Hash[Symbol,MyUnion] } end @@ -214,6 +218,8 @@ module RailsJson QueryParamsAsStringListMapOutput: untyped + RenamedGreeting: untyped + SimpleStruct: untyped StreamingOperationInput: untyped diff --git a/codegen/projections/rails_json/spec/protocol_spec.rb b/codegen/projections/rails_json/spec/protocol_spec.rb index 818714a0c..47522510d 100644 --- a/codegen/projections/rails_json/spec/protocol_spec.rb +++ b/codegen/projections/rails_json/spec/protocol_spec.rb @@ -25,9 +25,8 @@ module RailsJson it 'rails_json_serializes_bad_names' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/BadName/abc_value') + expect(request.uri.path).to eq('/BadName/abc_value') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"member":{"__123foo":"foo value"}}')) @@ -50,8 +49,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"member":{"__123foo":"foo value"}}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{"member":{"__123foo":"foo value"}}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -97,11 +97,10 @@ module RailsJson it 'RailsJsonAllQueryStringTypes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/AllQueryStringTypesInput') + expect(request.uri.path).to eq('/AllQueryStringTypesInput') expected_query = ::CGI.parse(['String=Hello%20there', 'StringList=a', 'StringList=b', 'StringList=c', 'StringSet=a', 'StringSet=b', 'StringSet=c', 'Byte=1', 'Short=2', 'Integer=3', 'IntegerList=1', 'IntegerList=2', 'IntegerList=3', 'IntegerSet=1', 'IntegerSet=2', 'IntegerSet=3', 'Long=4', 'Float=1.1', 'Double=1.1', 'DoubleList=1.1', 'DoubleList=2.1', 'DoubleList=3.1', 'Boolean=true', 'BooleanList=true', 'BooleanList=false', 'BooleanList=true', 'Timestamp=1970-01-01T00%3A00%3A01Z', 'TimestampList=1970-01-01T00%3A00%3A01Z', 'TimestampList=1970-01-01T00%3A00%3A02Z', 'TimestampList=1970-01-01T00%3A00%3A03Z', 'Enum=Foo', 'EnumList=Foo', 'EnumList=Baz', 'EnumList=Bar', 'QueryParamsStringKeyA=Foo', 'QueryParamsStringKeyB=Bar'].join('&')) - actual_query = ::CGI.parse(request_uri.query) + actual_query = ::CGI.parse(request.uri.query) expected_query.each do |k, v| expect(actual_query[k]).to eq(v) end @@ -178,16 +177,15 @@ module RailsJson it 'RailsJsonConstantAndVariableQueryStringMissingOneValue' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/ConstantAndVariableQueryString') + expect(request.uri.path).to eq('/ConstantAndVariableQueryString') expected_query = ::CGI.parse(['foo=bar', 'baz=bam'].join('&')) - actual_query = ::CGI.parse(request_uri.query) + actual_query = ::CGI.parse(request.uri.query) expected_query.each do |k, v| expect(actual_query[k]).to eq(v) end forbid_query = ['maybeSet'] - actual_query = ::CGI.parse(request_uri.query) + actual_query = ::CGI.parse(request.uri.query) forbid_query.each do |query| expect(actual_query.key?(query)).to be false end @@ -204,11 +202,10 @@ module RailsJson it 'RailsJsonConstantAndVariableQueryStringAllValues' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/ConstantAndVariableQueryString') + expect(request.uri.path).to eq('/ConstantAndVariableQueryString') expected_query = ::CGI.parse(['foo=bar', 'baz=bam', 'maybeSet=yes'].join('&')) - actual_query = ::CGI.parse(request_uri.query) + actual_query = ::CGI.parse(request.uri.query) expected_query.each do |k, v| expect(actual_query[k]).to eq(v) end @@ -233,11 +230,10 @@ module RailsJson it 'RailsJsonConstantQueryString' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/ConstantQueryString/hi') + expect(request.uri.path).to eq('/ConstantQueryString/hi') expected_query = ::CGI.parse(['foo=bar', 'hello'].join('&')) - actual_query = ::CGI.parse(request_uri.query) + actual_query = ::CGI.parse(request.uri.query) expected_query.each do |k, v| expect(actual_query[k]).to eq(v) end @@ -261,9 +257,8 @@ module RailsJson it 'RailsJsonDocumentTypeInputWithObject' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('PUT') - expect(request_uri.path).to eq('/DocumentType') + expect(request.uri.path).to eq('/DocumentType') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "string_value": "string", @@ -284,9 +279,8 @@ module RailsJson it 'RailsJsonDocumentInputWithString' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('PUT') - expect(request_uri.path).to eq('/DocumentType') + expect(request.uri.path).to eq('/DocumentType') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "string_value": "string", @@ -305,9 +299,8 @@ module RailsJson it 'RailsJsonDocumentInputWithNumber' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('PUT') - expect(request_uri.path).to eq('/DocumentType') + expect(request.uri.path).to eq('/DocumentType') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "string_value": "string", @@ -326,9 +319,8 @@ module RailsJson it 'RailsJsonDocumentInputWithBoolean' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('PUT') - expect(request_uri.path).to eq('/DocumentType') + expect(request.uri.path).to eq('/DocumentType') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "string_value": "string", @@ -347,9 +339,8 @@ module RailsJson it 'RailsJsonDocumentInputWithList' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('PUT') - expect(request_uri.path).to eq('/DocumentType') + expect(request.uri.path).to eq('/DocumentType') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "string_value": "string", @@ -387,13 +378,14 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "string_value": "string", "document_value": { "foo": "bar" } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -409,11 +401,12 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "string_value": "string", "document_value": "hello" }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -429,11 +422,12 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "string_value": "string", "document_value": 10 }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -449,11 +443,12 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "string_value": "string", "document_value": false }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -469,14 +464,15 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "string_value": "string", "document_value": [ true, false ] }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -591,9 +587,8 @@ module RailsJson it 'RailsJsonDocumentTypeAsPayloadInput' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('PUT') - expect(request_uri.path).to eq('/DocumentTypeAsPayload') + expect(request.uri.path).to eq('/DocumentTypeAsPayload') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "foo": "bar" @@ -610,9 +605,8 @@ module RailsJson it 'RailsJsonDocumentTypeAsPayloadInputString' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('PUT') - expect(request_uri.path).to eq('/DocumentTypeAsPayload') + expect(request.uri.path).to eq('/DocumentTypeAsPayload') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('"hello"')) Hearth::Output.new @@ -631,10 +625,11 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "foo": "bar" }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -649,8 +644,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('"hello"') + response.headers['Content-Type'] = 'application/json' + response.body.write('"hello"') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -710,8 +706,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -729,10 +726,11 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "foo": true }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -751,8 +749,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('') + response.headers['Content-Type'] = 'application/json' + response.body.write('') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -835,10 +834,9 @@ module RailsJson it 'RailsJsonEndpointTrait' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.host).to eq('foo.example.com') - expect(request_uri.path).to eq('/endpoint') + expect(request.uri.host).to eq('foo.example.com') + expect(request.uri.path).to eq('/endpoint') Hearth::Output.new end opts = {middleware: middleware} @@ -861,10 +859,9 @@ module RailsJson it 'RailsJsonEndpointTraitWithHostLabel' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.host).to eq('foo.bar.example.com') - expect(request_uri.path).to eq('/endpointwithhostlabel') + expect(request.uri.host).to eq('foo.bar.example.com') + expect(request.uri.path).to eq('/endpointwithhostlabel') expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"label_member": "bar"}')) Hearth::Output.new end @@ -887,10 +884,12 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 400 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json', 'x-smithy-rails-error' => 'InvalidGreeting' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.headers['x-smithy-rails-error'] = 'InvalidGreeting' + response.body.write('{ "message": "Hi" }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -911,13 +910,15 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 400 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json', 'x-smithy-rails-error' => 'ComplexError' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.headers['x-smithy-rails-error'] = 'ComplexError' + response.body.write('{ "top_level": "Top level", "nested": { "Fooooo": "bar" } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -938,9 +939,11 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 400 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json', 'x-smithy-rails-error' => 'ComplexError' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.headers['x-smithy-rails-error'] = 'ComplexError' + response.body.write('{ }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -964,9 +967,8 @@ module RailsJson it 'RailsJsonHttpPayloadTraitsWithBlob' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/HttpPayloadTraits') + expect(request.uri.path).to eq('/HttpPayloadTraits') { 'Content-Type' => 'application/octet-stream', 'X-Foo' => 'Foo' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(request.body.read).to eq('blobby blob blob') @@ -983,9 +985,8 @@ module RailsJson it 'RailsJsonHttpPayloadTraitsWithNoBlobBody' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/HttpPayloadTraits') + expect(request.uri.path).to eq('/HttpPayloadTraits') { 'X-Foo' => 'Foo' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(request.body.read).to eq('') Hearth::Output.new @@ -1004,8 +1005,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'X-Foo' => 'Foo' }) - response.body = StringIO.new('blobby blob blob') + response.headers['X-Foo'] = 'Foo' + response.body.write('blobby blob blob') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1021,8 +1023,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'X-Foo' => 'Foo' }) - response.body = StringIO.new('') + response.headers['X-Foo'] = 'Foo' + response.body.write('') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1080,9 +1083,8 @@ module RailsJson it 'RailsJsonHttpPayloadTraitsWithMediaTypeWithBlob' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/HttpPayloadTraitsWithMediaType') + expect(request.uri.path).to eq('/HttpPayloadTraitsWithMediaType') { 'Content-Type' => 'text/plain', 'X-Foo' => 'Foo' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(request.body.read).to eq('blobby blob blob') @@ -1103,8 +1105,10 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'text/plain', 'X-Foo' => 'Foo' }) - response.body = StringIO.new('blobby blob blob') + response.headers['Content-Type'] = 'text/plain' + response.headers['X-Foo'] = 'Foo' + response.body.write('blobby blob blob') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1147,9 +1151,8 @@ module RailsJson it 'RailsJsonHttpPayloadWithStructure' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('PUT') - expect(request_uri.path).to eq('/HttpPayloadWithStructure') + expect(request.uri.path).to eq('/HttpPayloadWithStructure') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ @@ -1175,11 +1178,12 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "greeting": "hello", "name": "Phreddy" }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1228,9 +1232,8 @@ module RailsJson it 'RailsJsonHttpPrefixHeadersArePresent' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/HttpPrefixHeaders') + expect(request.uri.path).to eq('/HttpPrefixHeaders') { 'X-Foo' => 'Foo', 'X-Foo-Abc' => 'Abc value', 'X-Foo-Def' => 'Def value' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(request.body.read).to eq('') Hearth::Output.new @@ -1249,9 +1252,8 @@ module RailsJson it 'RailsJsonHttpPrefixHeadersAreNotPresent' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/HttpPrefixHeaders') + expect(request.uri.path).to eq('/HttpPrefixHeaders') { 'X-Foo' => 'Foo' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(request.body.read).to eq('') Hearth::Output.new @@ -1273,7 +1275,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'X-Foo' => 'Foo', 'X-Foo-Abc' => 'Abc value', 'X-Foo-Def' => 'Def value' }) + response.headers['X-Foo'] = 'Foo' + response.headers['X-Foo-Abc'] = 'Abc value' + response.headers['X-Foo-Def'] = 'Def value' Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1326,7 +1330,8 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Hello' => 'Hello', 'X-Foo' => 'Foo' }) + response.headers['Hello'] = 'Hello' + response.headers['X-Foo'] = 'Foo' Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1375,9 +1380,8 @@ module RailsJson it 'RailsJsonSupportsNaNFloatLabels' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/FloatHttpLabels/NaN/NaN') + expect(request.uri.path).to eq('/FloatHttpLabels/NaN/NaN') expect(request.body.read).to eq('') Hearth::Output.new end @@ -1392,9 +1396,8 @@ module RailsJson it 'RailsJsonSupportsInfinityFloatLabels' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/FloatHttpLabels/Infinity/Infinity') + expect(request.uri.path).to eq('/FloatHttpLabels/Infinity/Infinity') expect(request.body.read).to eq('') Hearth::Output.new end @@ -1409,9 +1412,8 @@ module RailsJson it 'RailsJsonSupportsNegativeInfinityFloatLabels' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/FloatHttpLabels/-Infinity/-Infinity') + expect(request.uri.path).to eq('/FloatHttpLabels/-Infinity/-Infinity') expect(request.body.read).to eq('') Hearth::Output.new end @@ -1433,9 +1435,8 @@ module RailsJson it 'RailsJsonHttpRequestWithGreedyLabelInPath' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/HttpRequestWithGreedyLabelInPath/foo/hello%2Fescape/baz/there/guy') + expect(request.uri.path).to eq('/HttpRequestWithGreedyLabelInPath/foo/hello%2Fescape/baz/there/guy') expect(request.body.read).to eq('') Hearth::Output.new end @@ -1457,9 +1458,8 @@ module RailsJson it 'RailsJsonInputWithHeadersAndAllParams' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/HttpRequestWithLabels/string/1/2/3/4.1/5.1/true/2019-12-16T23%3A48%3A18Z') + expect(request.uri.path).to eq('/HttpRequestWithLabels/string/1/2/3/4.1/5.1/true/2019-12-16T23%3A48%3A18Z') expect(request.body.read).to eq('') Hearth::Output.new end @@ -1480,9 +1480,8 @@ module RailsJson it 'RailsJsonHttpRequestLabelEscaping' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/HttpRequestWithLabels/%25%3A%2F%3F%23%5B%5D%40%21%24%26%27%28%29%2A%2B%2C%3B%3D%F0%9F%98%B9/1/2/3/4.1/5.1/true/2019-12-16T23%3A48%3A18Z') + expect(request.uri.path).to eq('/HttpRequestWithLabels/%25%3A%2F%3F%23%5B%5D%40%21%24%26%27%28%29%2A%2B%2C%3B%3D%F0%9F%98%B9/1/2/3/4.1/5.1/true/2019-12-16T23%3A48%3A18Z') expect(request.body.read).to eq('') Hearth::Output.new end @@ -1510,9 +1509,8 @@ module RailsJson it 'RailsJsonHttpRequestWithLabelsAndTimestampFormat' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/HttpRequestWithLabelsAndTimestampFormat/1576540098/Mon%2C%2016%20Dec%202019%2023%3A48%3A18%20GMT/2019-12-16T23%3A48%3A18Z/2019-12-16T23%3A48%3A18Z/1576540098/Mon%2C%2016%20Dec%202019%2023%3A48%3A18%20GMT/2019-12-16T23%3A48%3A18Z') + expect(request.uri.path).to eq('/HttpRequestWithLabelsAndTimestampFormat/1576540098/Mon%2C%2016%20Dec%202019%2023%3A48%3A18%20GMT/2019-12-16T23%3A48%3A18Z/2019-12-16T23%3A48%3A18Z/1576540098/Mon%2C%2016%20Dec%202019%2023%3A48%3A18%20GMT/2019-12-16T23%3A48%3A18Z') expect(request.body.read).to eq('') Hearth::Output.new end @@ -1544,8 +1542,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 201 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1562,7 +1561,8 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 201 - response.body = StringIO.new('') + response.body.write('') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1628,8 +1628,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1646,7 +1647,8 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.body = StringIO.new('') + response.body.write('') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1707,9 +1709,8 @@ module RailsJson it 'RailsJsonInputAndOutputWithStringHeaders' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/InputAndOutputWithHeaders') + expect(request.uri.path).to eq('/InputAndOutputWithHeaders') { 'X-String' => 'Hello', 'X-StringList' => 'a, b, c', 'X-StringSet' => 'a, b, c' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(request.body.read).to eq('') Hearth::Output.new @@ -1734,9 +1735,8 @@ module RailsJson it 'RailsJsonInputAndOutputWithNumericHeaders' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/InputAndOutputWithHeaders') + expect(request.uri.path).to eq('/InputAndOutputWithHeaders') { 'X-Byte' => '1', 'X-Double' => '1.1', 'X-Float' => '1.1', 'X-Integer' => '123', 'X-IntegerList' => '1, 2, 3', 'X-Long' => '123', 'X-Short' => '123' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(request.body.read).to eq('') Hearth::Output.new @@ -1761,9 +1761,8 @@ module RailsJson it 'RailsJsonInputAndOutputWithBooleanHeaders' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/InputAndOutputWithHeaders') + expect(request.uri.path).to eq('/InputAndOutputWithHeaders') { 'X-Boolean1' => 'true', 'X-Boolean2' => 'false', 'X-BooleanList' => 'true, false, true' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(request.body.read).to eq('') Hearth::Output.new @@ -1784,9 +1783,8 @@ module RailsJson it 'RailsJsonInputAndOutputWithEnumHeaders' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/InputAndOutputWithHeaders') + expect(request.uri.path).to eq('/InputAndOutputWithHeaders') { 'X-Enum' => 'Foo', 'X-EnumList' => 'Foo, Bar, Baz' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(request.body.read).to eq('') Hearth::Output.new @@ -1810,8 +1808,11 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'X-String' => 'Hello', 'X-StringList' => 'a, b, c', 'X-StringSet' => 'a, b, c' }) - response.body = StringIO.new('') + response.headers['X-String'] = 'Hello' + response.headers['X-StringList'] = 'a, b, c' + response.headers['X-StringSet'] = 'a, b, c' + response.body.write('') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1836,8 +1837,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'X-StringList' => '"b,c", "\"def\"", a' }) - response.body = StringIO.new('') + response.headers['X-StringList'] = '"b,c", "\"def\"", a' + response.body.write('') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1856,8 +1858,15 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'X-Byte' => '1', 'X-Double' => '1.1', 'X-Float' => '1.1', 'X-Integer' => '123', 'X-IntegerList' => '1, 2, 3', 'X-Long' => '123', 'X-Short' => '123' }) - response.body = StringIO.new('') + response.headers['X-Byte'] = '1' + response.headers['X-Double'] = '1.1' + response.headers['X-Float'] = '1.1' + response.headers['X-Integer'] = '123' + response.headers['X-IntegerList'] = '1, 2, 3' + response.headers['X-Long'] = '123' + response.headers['X-Short'] = '123' + response.body.write('') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1882,8 +1891,11 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'X-Boolean1' => 'true', 'X-Boolean2' => 'false', 'X-BooleanList' => 'true, false, true' }) - response.body = StringIO.new('') + response.headers['X-Boolean1'] = 'true' + response.headers['X-Boolean2'] = 'false' + response.headers['X-BooleanList'] = 'true, false, true' + response.body.write('') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -1904,8 +1916,10 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'X-Enum' => 'Foo', 'X-EnumList' => 'Foo, Bar, Baz' }) - response.body = StringIO.new('') + response.headers['X-Enum'] = 'Foo' + response.headers['X-EnumList'] = 'Foo, Bar, Baz' + response.body.write('') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -2084,9 +2098,8 @@ module RailsJson it 'RailsJsonEnums' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/jsonenums') + expect(request.uri.path).to eq('/jsonenums') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "foo_enum1": "Foo", @@ -2135,8 +2148,8 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "foo_enum1": "Foo", "foo_enum2": "0", "foo_enum3": "1", @@ -2153,6 +2166,7 @@ module RailsJson "zero": "0" } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -2234,9 +2248,8 @@ module RailsJson it 'RailsJsonJsonMaps' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/JsonMaps') + expect(request.uri.path).to eq('/JsonMaps') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "dense_struct_map": { @@ -2283,9 +2296,8 @@ module RailsJson it 'RailsJsonSerializesNullMapValues' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/JsonMaps') + expect(request.uri.path).to eq('/JsonMaps') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "sparse_boolean_map": { @@ -2324,9 +2336,8 @@ module RailsJson it 'RailsJsonSerializesZeroValuesInMaps' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/JsonMaps') + expect(request.uri.path).to eq('/JsonMaps') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "dense_number_map": { @@ -2365,9 +2376,8 @@ module RailsJson it 'RailsJsonSerializesSparseSetMap' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/JsonMaps') + expect(request.uri.path).to eq('/JsonMaps') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "sparse_set_map": { @@ -2395,9 +2405,8 @@ module RailsJson it 'RailsJsonSerializesDenseSetMap' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/JsonMaps') + expect(request.uri.path).to eq('/JsonMaps') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "dense_set_map": { @@ -2425,9 +2434,8 @@ module RailsJson it 'RailsJsonSerializesSparseSetMapAndRetainsNull' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/JsonMaps') + expect(request.uri.path).to eq('/JsonMaps') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "sparse_set_map": { @@ -2461,8 +2469,8 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "dense_struct_map": { "foo": { "hi": "there" @@ -2480,6 +2488,7 @@ module RailsJson } } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -2509,8 +2518,8 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "sparse_boolean_map": { "x": null }, @@ -2524,6 +2533,7 @@ module RailsJson "x": null } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -2549,8 +2559,8 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "dense_number_map": { "x": 0 }, @@ -2564,6 +2574,7 @@ module RailsJson "x": false } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -2589,13 +2600,14 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "sparse_set_map": { "x": [], "y": ["a", "b"] } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -2618,13 +2630,14 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "dense_set_map": { "x": [], "y": ["a", "b"] } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -2647,14 +2660,15 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "sparse_set_map": { "x": [], "y": ["a", "b"], "z": null } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -2679,14 +2693,15 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "dense_set_map": { "x": [], "y": ["a", "b"], "z": null } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -2971,9 +2986,8 @@ module RailsJson it 'RailsJsonSerializeStringUnionValue' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/jsonunions') + expect(request.uri.path).to eq('/jsonunions') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "contents": { @@ -2994,9 +3008,8 @@ module RailsJson it 'RailsJsonSerializeBooleanUnionValue' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/jsonunions') + expect(request.uri.path).to eq('/jsonunions') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "contents": { @@ -3017,9 +3030,8 @@ module RailsJson it 'RailsJsonSerializeNumberUnionValue' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/jsonunions') + expect(request.uri.path).to eq('/jsonunions') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "contents": { @@ -3040,9 +3052,8 @@ module RailsJson it 'RailsJsonSerializeBlobUnionValue' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/jsonunions') + expect(request.uri.path).to eq('/jsonunions') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "contents": { @@ -3063,9 +3074,8 @@ module RailsJson it 'RailsJsonSerializeTimestampUnionValue' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/jsonunions') + expect(request.uri.path).to eq('/jsonunions') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "contents": { @@ -3086,9 +3096,8 @@ module RailsJson it 'RailsJsonSerializeEnumUnionValue' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/jsonunions') + expect(request.uri.path).to eq('/jsonunions') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "contents": { @@ -3109,9 +3118,8 @@ module RailsJson it 'RailsJsonSerializeListUnionValue' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/jsonunions') + expect(request.uri.path).to eq('/jsonunions') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "contents": { @@ -3135,9 +3143,8 @@ module RailsJson it 'RailsJsonSerializeMapUnionValue' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/jsonunions') + expect(request.uri.path).to eq('/jsonunions') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "contents": { @@ -3164,9 +3171,8 @@ module RailsJson it 'RailsJsonSerializeStructureUnionValue' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/jsonunions') + expect(request.uri.path).to eq('/jsonunions') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "contents": { @@ -3186,6 +3192,32 @@ module RailsJson } }, **opts) end + # Serializes a renamed structure union value + # + it 'RailsJsonSerializeRenamedStructureUnionValue' do + middleware = Hearth::MiddlewareBuilder.before_send do |input, context| + request = context.request + expect(request.http_method).to eq('POST') + expect(request.uri.path).to eq('/jsonunions') + { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } + expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ + "contents": { + "renamed_structure_value": { + "salutation": "hello!" + } + } + }')) + Hearth::Output.new + end + opts = {middleware: middleware} + client.json_unions({ + contents: { + renamed_structure_value: { + salutation: "hello!" + } + } + }, **opts) + end end describe 'responses' do @@ -3195,12 +3227,13 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "contents": { "string_value": "foo" } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -3217,12 +3250,13 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "contents": { "boolean_value": true } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -3239,12 +3273,13 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "contents": { "number_value": 1 } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -3261,12 +3296,13 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "contents": { "blob_value": "Zm9v" } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -3283,12 +3319,13 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "contents": { "timestamp_value": "2014-04-29T18:30:38Z" } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -3305,12 +3342,13 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "contents": { "enum_value": "Foo" } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -3327,12 +3365,13 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "contents": { "list_value": ["foo", "bar"] } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -3352,8 +3391,8 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "contents": { "map_value": { "foo": "bar", @@ -3361,6 +3400,7 @@ module RailsJson } } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -3380,14 +3420,15 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "contents": { "structure_value": { "hi": "hello" } } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -3611,9 +3652,8 @@ module RailsJson it 'rails_json_rails_json_serializes_string_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"string":"abc xyz"}')) @@ -3629,9 +3669,8 @@ module RailsJson it 'rails_json_serializes_string_shapes_with_jsonvalue_trait' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"json_value":"{\"string\":\"value\",\"number\":1234.5,\"boolTrue\":true,\"boolFalse\":false,\"array\":[1,2,3,4],\"object\":{\"key\":\"value\"},\"null\":null}"}')) @@ -3647,9 +3686,8 @@ module RailsJson it 'rails_json_serializes_integer_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"integer":1234}')) @@ -3665,9 +3703,8 @@ module RailsJson it 'rails_json_serializes_long_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"long":999999999999}')) @@ -3683,9 +3720,8 @@ module RailsJson it 'rails_json_serializes_float_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"float":1234.5}')) @@ -3701,9 +3737,8 @@ module RailsJson it 'rails_json_serializes_double_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"double":1234.5}')) @@ -3719,9 +3754,8 @@ module RailsJson it 'rails_json_serializes_blob_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"blob":"YmluYXJ5LXZhbHVl"}')) @@ -3737,9 +3771,8 @@ module RailsJson it 'rails_json_serializes_boolean_shapes_true' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"boolean":true}')) @@ -3755,9 +3788,8 @@ module RailsJson it 'rails_json_serializes_boolean_shapes_false' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"boolean":false}')) @@ -3773,9 +3805,8 @@ module RailsJson it 'rails_json_serializes_timestamp_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"timestamp":"2000-01-02T20:34:56Z"}')) @@ -3786,14 +3817,30 @@ module RailsJson timestamp: Time.at(946845296) }, **opts) end + # Serializes fractional timestamp shapes + # + it 'rails_json_serializes_fractional_timestamp_shapes' do + middleware = Hearth::MiddlewareBuilder.before_send do |input, context| + request = context.request + expect(request.http_method).to eq('POST') + expect(request.uri.path).to eq('/') + { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } + ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } + expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"timestamp":"2000-01-02T20:34:56.123Z"}')) + Hearth::Output.new + end + opts = {middleware: middleware} + client.kitchen_sink_operation({ + timestamp: Time.at(946845296, 123, :millisecond) + }, **opts) + end # Serializes timestamp shapes with iso8601 timestampFormat # it 'rails_json_serializes_timestamp_shapes_with_iso8601_timestampformat' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"iso8601_timestamp":"2000-01-02T20:34:56Z"}')) @@ -3809,9 +3856,8 @@ module RailsJson it 'rails_json_serializes_timestamp_shapes_with_httpdate_timestampformat' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"httpdate_timestamp":"Sun, 02 Jan 2000 20:34:56 GMT"}')) @@ -3827,9 +3873,8 @@ module RailsJson it 'rails_json_serializes_timestamp_shapes_with_unixtimestamp_timestampformat' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"unix_timestamp":946845296}')) @@ -3845,9 +3890,8 @@ module RailsJson it 'rails_json_serializes_list_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"list_of_strings":["abc","mno","xyz"]}')) @@ -3867,9 +3911,8 @@ module RailsJson it 'rails_json_serializes_empty_list_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"list_of_strings":[]}')) @@ -3887,9 +3930,8 @@ module RailsJson it 'rails_json_serializes_list_of_map_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"list_of_maps_of_strings":[{"foo":"bar"},{"abc":"xyz"},{"red":"blue"}]}')) @@ -3915,9 +3957,8 @@ module RailsJson it 'rails_json_serializes_list_of_structure_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"list_of_structs":[{"value":"abc"},{"value":"mno"},{"value":"xyz"}]}')) @@ -3943,9 +3984,8 @@ module RailsJson it 'rails_json_serializes_list_of_recursive_structure_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"recursive_list":[{"recursive_list":[{"recursive_list":[{"integer":123}]}]}]}')) @@ -3973,9 +4013,8 @@ module RailsJson it 'rails_json_serializes_map_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"map_of_strings":{"abc":"xyz","mno":"hjk"}}')) @@ -3994,9 +4033,8 @@ module RailsJson it 'rails_json_serializes_empty_map_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"map_of_strings":{}}')) @@ -4014,9 +4052,8 @@ module RailsJson it 'rails_json_serializes_map_of_list_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"map_of_lists_of_strings":{"abc":["abc","xyz"],"mno":["xyz","abc"]}}')) @@ -4041,9 +4078,8 @@ module RailsJson it 'rails_json_serializes_map_of_structure_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"map_of_structs":{"key1":{"value":"value-1"},"key2":{"value":"value-2"}}}')) @@ -4066,9 +4102,8 @@ module RailsJson it 'rails_json_serializes_map_of_recursive_structure_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"recursive_map":{"key1":{"recursive_map":{"key2":{"recursive_map":{"key3":{"boolean":false}}}}}}}')) @@ -4096,9 +4131,8 @@ module RailsJson it 'rails_json_serializes_structure_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"simple_struct":{"value":"abc"}}')) @@ -4116,9 +4150,8 @@ module RailsJson it 'rails_json_serializes_structure_members_with_locationname_traits' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"struct_with_location_name":{"RenamedMember":"some-value"}}')) @@ -4136,9 +4169,8 @@ module RailsJson it 'rails_json_serializes_empty_structure_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"simple_struct":{}}')) @@ -4156,9 +4188,8 @@ module RailsJson it 'rails_json_serializes_structure_which_have_no_members' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"empty_struct":{}}')) @@ -4176,9 +4207,8 @@ module RailsJson it 'rails_json_serializes_recursive_structure_shapes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/') + expect(request.uri.path).to eq('/') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } ['Content-Length'].each { |k| expect(request.headers.key?(k)).to be(true) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"string":"top-value","boolean":false,"recursive_struct":{"string":"nested-value","boolean":true,"recursive_list":[{"string":"string-only"},{"recursive_struct":{"map_of_strings":{"color":"red","size":"large"}}}]}}')) @@ -4216,8 +4246,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4232,8 +4263,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"string":"string-value"}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{"string":"string-value"}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4248,8 +4280,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"integer":1234}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{"integer":1234}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4264,8 +4297,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"long":1234567890123456789}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{"long":1234567890123456789}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4280,8 +4314,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"float":1234.5}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{"float":1234.5}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4296,8 +4331,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"double":123456789.12345679}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{"double":123456789.12345679}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4312,8 +4348,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"boolean":true}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{"boolean":true}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4328,8 +4365,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"boolean":false}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{"boolean":false}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4344,8 +4382,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"blob":"YmluYXJ5LXZhbHVl"}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{"blob":"YmluYXJ5LXZhbHVl"}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4360,8 +4399,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"timestamp":"2000-01-02T20:34:56Z"}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{"timestamp":"2000-01-02T20:34:56Z"}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4370,14 +4410,32 @@ module RailsJson timestamp: Time.at(946845296) }) end + # Parses fractional timestamp shapes + # + it 'rails_json_parses_fractional_timestamp_shapes' do + middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| + response = context.response + response.status = 200 + response.headers['Content-Type'] = 'application/json' + response.body.write('{"timestamp":"2000-01-02T20:34:56.123Z"}') + response.body.rewind + Hearth::Output.new + end + middleware.remove_send.remove_build.remove_retry + output = client.kitchen_sink_operation({}, middleware: middleware) + expect(output.data.to_h).to eq({ + timestamp: Time.at(946845296, 123, :millisecond) + }) + end # Parses iso8601 timestamps # it 'rails_json_parses_iso8601_timestamps' do middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"iso8601_timestamp":"2000-01-02T20:34:56Z"}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{"iso8601_timestamp":"2000-01-02T20:34:56Z"}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4392,8 +4450,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"httpdate_timestamp":"Sun, 02 Jan 2000 20:34:56.000 GMT"}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{"httpdate_timestamp":"Sun, 02 Jan 2000 20:34:56.000 GMT"}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4402,14 +4461,32 @@ module RailsJson httpdate_timestamp: Time.at(946845296) }) end + # Parses fractional httpdate timestamps + # + it 'rails_json_parses_fractional_httpdate_timestamps' do + middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| + response = context.response + response.status = 200 + response.headers['Content-Type'] = 'application/json' + response.body.write('{"httpdate_timestamp":"Sun, 02 Jan 2000 20:34:56.123 GMT"}') + response.body.rewind + Hearth::Output.new + end + middleware.remove_send.remove_build.remove_retry + output = client.kitchen_sink_operation({}, middleware: middleware) + expect(output.data.to_h).to eq({ + httpdate_timestamp: Time.at(946845296, 123, :millisecond) + }) + end # Parses list shapes # it 'rails_json_parses_list_shapes' do middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"list_of_strings":["abc","mno","xyz"]}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{"list_of_strings":["abc","mno","xyz"]}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4428,8 +4505,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"list_of_maps_of_strings":[{"size":"large"},{"color":"red"}]}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{"list_of_maps_of_strings":[{"size":"large"},{"color":"red"}]}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4451,8 +4529,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"list_of_lists":[["abc","mno","xyz"],["hjk","qrs","tuv"]]}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{"list_of_lists":[["abc","mno","xyz"],["hjk","qrs","tuv"]]}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4478,8 +4557,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"list_of_structs":[{"value":"value-1"},{"value":"value-2"}]}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{"list_of_structs":[{"value":"value-1"},{"value":"value-2"}]}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4501,8 +4581,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"recursive_list":[{"recursive_list":[{"recursive_list":[{"string":"value"}]}]}]}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{"recursive_list":[{"recursive_list":[{"recursive_list":[{"string":"value"}]}]}]}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4529,8 +4610,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"map_of_strings":{"size":"large","color":"red"}}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{"map_of_strings":{"size":"large","color":"red"}}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4548,8 +4630,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"map_of_lists_of_strings":{"sizes":["large","small"],"colors":["red","green"]}}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{"map_of_lists_of_strings":{"sizes":["large","small"],"colors":["red","green"]}}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4573,8 +4656,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"map_of_maps":{"sizes":{"large":"L","medium":"M"},"colors":{"red":"R","blue":"B"}}}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{"map_of_maps":{"sizes":{"large":"L","medium":"M"},"colors":{"red":"R","blue":"B"}}}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4598,8 +4682,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"map_of_structs":{"size":{"value":"small"},"color":{"value":"red"}}}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{"map_of_structs":{"size":{"value":"small"},"color":{"value":"red"}}}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4621,8 +4706,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{"recursive_map":{"key-1":{"recursive_map":{"key-2":{"recursive_map":{"key-3":{"string":"value"}}}}}}}') + response.headers['Content-Type'] = 'application/json' + response.body.write('{"recursive_map":{"key-1":{"recursive_map":{"key-2":{"recursive_map":{"key-3":{"string":"value"}}}}}}}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4649,8 +4735,10 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json', 'X-Amzn-Requestid' => 'amazon-uniq-request-id' }) - response.body = StringIO.new('{}') + response.headers['Content-Type'] = 'application/json' + response.headers['X-Amzn-Requestid'] = 'amazon-uniq-request-id' + response.body.write('{}') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -4822,6 +4910,22 @@ module RailsJson timestamp: Time.at(946845296) }) end + # Parses fractional timestamp shapes + # + it 'stubs rails_json_parses_fractional_timestamp_shapes' do + middleware = Hearth::MiddlewareBuilder.after_send do |input, context| + response = context.response + expect(response.status).to eq(200) + end + middleware.remove_build.remove_retry + client.stub_responses(:kitchen_sink_operation, { + timestamp: Time.at(946845296, 123, :millisecond) + }) + output = client.kitchen_sink_operation({}, middleware: middleware) + expect(output.data.to_h).to eq({ + timestamp: Time.at(946845296, 123, :millisecond) + }) + end # Parses iso8601 timestamps # it 'stubs rails_json_parses_iso8601_timestamps' do @@ -4854,6 +4958,22 @@ module RailsJson httpdate_timestamp: Time.at(946845296) }) end + # Parses fractional httpdate timestamps + # + it 'stubs rails_json_parses_fractional_httpdate_timestamps' do + middleware = Hearth::MiddlewareBuilder.after_send do |input, context| + response = context.response + expect(response.status).to eq(200) + end + middleware.remove_build.remove_retry + client.stub_responses(:kitchen_sink_operation, { + httpdate_timestamp: Time.at(946845296, 123, :millisecond) + }) + output = client.kitchen_sink_operation({}, middleware: middleware) + expect(output.data.to_h).to eq({ + httpdate_timestamp: Time.at(946845296, 123, :millisecond) + }) + end # Parses list shapes # it 'stubs rails_json_parses_list_shapes' do @@ -5204,9 +5324,8 @@ module RailsJson it 'RailsJsonMediaTypeHeaderInputBase64' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/MediaTypeHeader') + expect(request.uri.path).to eq('/MediaTypeHeader') { 'X-Json' => 'dHJ1ZQ==' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(request.body.read).to eq('') Hearth::Output.new @@ -5225,8 +5344,9 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'X-Json' => 'dHJ1ZQ==' }) - response.body = StringIO.new('') + response.headers['X-Json'] = 'dHJ1ZQ==' + response.body.write('') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -5266,9 +5386,8 @@ module RailsJson it 'rails_json_nested_attributes' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/nestedattributes') + expect(request.uri.path).to eq('/nestedattributes') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"simple_struct_attributes":{"value":"simple struct value"}}')) Hearth::Output.new @@ -5292,9 +5411,8 @@ module RailsJson it 'RailsJsonNullAndEmptyHeaders' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/NullAndEmptyHeadersClient') + expect(request.uri.path).to eq('/NullAndEmptyHeadersClient') ['X-A', 'X-B', 'X-C'].each { |k| expect(request.headers.key?(k)).to be(false) } expect(request.body.read).to eq('') Hearth::Output.new @@ -5320,9 +5438,8 @@ module RailsJson it 'RailsJsonStructuresDontSerializeNullValues' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/nulloperation') + expect(request.uri.path).to eq('/nulloperation') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{}')) Hearth::Output.new @@ -5337,9 +5454,8 @@ module RailsJson it 'RailsJsonMapsSerializeNullValues' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/nulloperation') + expect(request.uri.path).to eq('/nulloperation') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "sparse_string_map": { @@ -5360,9 +5476,8 @@ module RailsJson it 'RailsJsonListsSerializeNull' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/nulloperation') + expect(request.uri.path).to eq('/nulloperation') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{ "sparse_string_list": [ @@ -5387,10 +5502,11 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "string": null }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -5405,12 +5521,13 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "sparse_string_map": { "foo": null } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -5427,12 +5544,13 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'Content-Type' => 'application/json' }) - response.body = StringIO.new('{ + response.headers['Content-Type'] = 'application/json' + response.body.write('{ "sparse_string_list": [ null ] }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -5514,9 +5632,8 @@ module RailsJson it 'RailsJsonOmitsNullQuery' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/OmitsNullSerializesEmptyString') + expect(request.uri.path).to eq('/OmitsNullSerializesEmptyString') expect(request.body.read).to eq('') Hearth::Output.new end @@ -5530,11 +5647,10 @@ module RailsJson it 'RailsJsonSerializesEmptyQueryValue' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/OmitsNullSerializesEmptyString') + expect(request.uri.path).to eq('/OmitsNullSerializesEmptyString') expected_query = ::CGI.parse(['Empty='].join('&')) - actual_query = ::CGI.parse(request_uri.query) + actual_query = ::CGI.parse(request.uri.query) expected_query.each do |k, v| expect(actual_query[k]).to eq(v) end @@ -5558,9 +5674,8 @@ module RailsJson it 'rails_json_can_call_operation_with_no_input_or_output' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/operationwithoptionalinputoutput') + expect(request.uri.path).to eq('/operationwithoptionalinputoutput') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{}')) Hearth::Output.new @@ -5575,9 +5690,8 @@ module RailsJson it 'rails_json_can_call_operation_with_optional_input' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/operationwithoptionalinputoutput') + expect(request.uri.path).to eq('/operationwithoptionalinputoutput') { 'Content-Type' => 'application/json' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(JSON.parse(request.body.read)).to eq(JSON.parse('{"value":"Hi"}')) Hearth::Output.new @@ -5600,11 +5714,10 @@ module RailsJson allow(SecureRandom).to receive(:uuid).and_return('00000000-0000-4000-8000-000000000000') middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/QueryIdempotencyTokenAutoFill') + expect(request.uri.path).to eq('/QueryIdempotencyTokenAutoFill') expected_query = ::CGI.parse(['token=00000000-0000-4000-8000-000000000000'].join('&')) - actual_query = ::CGI.parse(request_uri.query) + actual_query = ::CGI.parse(request.uri.query) expected_query.each do |k, v| expect(actual_query[k]).to eq(v) end @@ -5622,11 +5735,10 @@ module RailsJson allow(SecureRandom).to receive(:uuid).and_return('00000000-0000-4000-8000-000000000000') middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/QueryIdempotencyTokenAutoFill') + expect(request.uri.path).to eq('/QueryIdempotencyTokenAutoFill') expected_query = ::CGI.parse(['token=00000000-0000-4000-8000-000000000000'].join('&')) - actual_query = ::CGI.parse(request_uri.query) + actual_query = ::CGI.parse(request.uri.query) expected_query.each do |k, v| expect(actual_query[k]).to eq(v) end @@ -5650,11 +5762,10 @@ module RailsJson it 'RailsJsonQueryParamsStringListMap' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/StringListMap') + expect(request.uri.path).to eq('/StringListMap') expected_query = ::CGI.parse(['corge=named', 'baz=bar', 'baz=qux'].join('&')) - actual_query = ::CGI.parse(request_uri.query) + actual_query = ::CGI.parse(request.uri.query) expected_query.each do |k, v| expect(actual_query[k]).to eq(v) end @@ -5688,9 +5799,8 @@ module RailsJson it 'RailsJsonTimestampFormatHeaders' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('POST') - expect(request_uri.path).to eq('/TimestampFormatHeaders') + expect(request.uri.path).to eq('/TimestampFormatHeaders') { 'X-defaultFormat' => 'Mon, 16 Dec 2019 23:48:18 GMT', 'X-memberDateTime' => '2019-12-16T23:48:18Z', 'X-memberEpochSeconds' => '1576540098', 'X-memberHttpDate' => 'Mon, 16 Dec 2019 23:48:18 GMT', 'X-targetDateTime' => '2019-12-16T23:48:18Z', 'X-targetEpochSeconds' => '1576540098', 'X-targetHttpDate' => 'Mon, 16 Dec 2019 23:48:18 GMT' }.each { |k, v| expect(request.headers[k]).to eq(v) } expect(request.body.read).to eq('') Hearth::Output.new @@ -5715,8 +5825,15 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'X-defaultFormat' => 'Mon, 16 Dec 2019 23:48:18 GMT', 'X-memberDateTime' => '2019-12-16T23:48:18Z', 'X-memberEpochSeconds' => '1576540098', 'X-memberHttpDate' => 'Mon, 16 Dec 2019 23:48:18 GMT', 'X-targetDateTime' => '2019-12-16T23:48:18Z', 'X-targetEpochSeconds' => '1576540098', 'X-targetHttpDate' => 'Mon, 16 Dec 2019 23:48:18 GMT' }) - response.body = StringIO.new('') + response.headers['X-defaultFormat'] = 'Mon, 16 Dec 2019 23:48:18 GMT' + response.headers['X-memberDateTime'] = '2019-12-16T23:48:18Z' + response.headers['X-memberEpochSeconds'] = '1576540098' + response.headers['X-memberHttpDate'] = 'Mon, 16 Dec 2019 23:48:18 GMT' + response.headers['X-targetDateTime'] = '2019-12-16T23:48:18Z' + response.headers['X-targetEpochSeconds'] = '1576540098' + response.headers['X-targetHttpDate'] = 'Mon, 16 Dec 2019 23:48:18 GMT' + response.body.write('') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry diff --git a/codegen/projections/rails_json/spec/request_id_spec.rb b/codegen/projections/rails_json/spec/request_id_spec.rb index 257d89c85..350607ef5 100644 --- a/codegen/projections/rails_json/spec/request_id_spec.rb +++ b/codegen/projections/rails_json/spec/request_id_spec.rb @@ -13,7 +13,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'x-request-id' => '123' }) + response.headers['x-request-id'] = '123' response.body = StringIO.new('{}') Hearth::Output.new end @@ -28,9 +28,8 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 400 - response.headers = Hearth::HTTP::Headers.new( - { 'x-smithy-rails-error' => 'InvalidGreeting', 'x-request-id' => '123' } - ) + response.headers['x-smithy-rails-error'] = 'InvalidGreeting' + response.headers['x-request-id'] = '123' response.body = StringIO.new('{}') Hearth::Output.new end diff --git a/codegen/projections/weather/lib/weather/builders.rb b/codegen/projections/weather/lib/weather/builders.rb index af8232e29..a0838e331 100644 --- a/codegen/projections/weather/lib/weather/builders.rb +++ b/codegen/projections/weather/lib/weather/builders.rb @@ -8,6 +8,7 @@ # WARNING ABOUT GENERATED CODE module Weather + # @api private module Builders # Operation Builder for GetCity diff --git a/codegen/projections/weather/lib/weather/client.rb b/codegen/projections/weather/lib/weather/client.rb index 4f778970e..ecf81e8c4 100644 --- a/codegen/projections/weather/lib/weather/client.rb +++ b/codegen/projections/weather/lib/weather/client.rb @@ -30,8 +30,6 @@ def initialize(config = Weather::Config.new, options = {}) @config = config @middleware = Hearth::MiddlewareBuilder.new(options[:middleware]) @stubs = Hearth::Stubbing::Stubs.new - @retry_quota = Hearth::Retry::RetryQuota.new - @client_rate_limiter = Hearth::Retry::ClientRateLimiter.new end # @param [Hash] params @@ -71,12 +69,8 @@ def get_city(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: [Errors::NoSuchResource]), @@ -84,7 +78,7 @@ def get_city(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::GetCity, stubs: @stubs, params_class: Params::GetCityOutput @@ -94,7 +88,7 @@ def get_city(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -143,12 +137,8 @@ def get_city_image(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: [Errors::NoSuchResource]), @@ -156,7 +146,7 @@ def get_city_image(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::GetCityImage, stubs: @stubs, params_class: Params::GetCityImageOutput @@ -166,7 +156,7 @@ def get_city_image(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -204,12 +194,8 @@ def get_current_time(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -217,7 +203,7 @@ def get_current_time(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::GetCurrentTime, stubs: @stubs, params_class: Params::GetCurrentTimeOutput @@ -227,7 +213,7 @@ def get_current_time(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -282,12 +268,8 @@ def get_forecast(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -295,7 +277,7 @@ def get_forecast(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::GetForecast, stubs: @stubs, params_class: Params::GetForecastOutput @@ -305,7 +287,7 @@ def get_forecast(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -365,12 +347,8 @@ def list_cities(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -378,7 +356,7 @@ def list_cities(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::ListCities, stubs: @stubs, params_class: Params::ListCitiesOutput @@ -388,7 +366,7 @@ def list_cities(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -433,12 +411,8 @@ def operation____789_bad_name(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: [Errors::NoSuchResource]), @@ -446,7 +420,7 @@ def operation____789_bad_name(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::Operation____789BadName, stubs: @stubs, params_class: Params::Struct____789BadNameOutput @@ -456,7 +430,7 @@ def operation____789_bad_name(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, diff --git a/codegen/projections/weather/lib/weather/config.rb b/codegen/projections/weather/lib/weather/config.rb index c7cafac68..48663c02c 100644 --- a/codegen/projections/weather/lib/weather/config.rb +++ b/codegen/projections/weather/lib/weather/config.rb @@ -9,31 +9,27 @@ module Weather # @!method initialize(*options) - # @option args [Boolean] :adaptive_retry_wait_to_fill (true) - # Used only in `adaptive` retry mode. When true, the request will sleep until there is sufficient client side capacity to retry the request. When false, the request will raise a `CapacityNotAvailableError` and will not retry instead of sleeping. - # # @option args [Boolean] :disable_host_prefix (false) # When `true`, does not perform host prefix injection using @endpoint's hostPrefix property. # # @option args [String] :endpoint # Endpoint of the service # - # @option args [Boolean] :http_wire_trace (false) - # Enable debug wire trace on http requests. + # @option args [Hearth::HTTP::Client] :http_client (Hearth::HTTP::Client.new) + # The HTTP Client to use for request transport. # # @option args [Symbol] :log_level (:info) - # Default log level to use - # - # @option args [Logger] :logger ($stdout) - # Logger to use for output + # The default log level to use with the Logger. # - # @option args [Integer] :max_attempts (3) - # An integer representing the maximum number of attempts that will be made for a single request, including the initial attempt. + # @option args [Logger] :logger (Logger.new($stdout, level: cfg.log_level)) + # The Logger instance to use for logging. # - # @option args [String] :retry_mode ('standard') - # Specifies which retry algorithm to use. Values are: - # * `standard` - A standardized set of retry rules across the AWS SDKs. This includes support for retry quotas, which limit the number of unsuccessful retries a client can make. - # * `adaptive` - An experimental retry mode that includes all the functionality of `standard` mode along with automatic client side throttling. This is a provisional mode that may change behavior in the future. + # @option args [Hearth::Retry::Strategy] :retry_strategy (Hearth::Retry::Standard.new) + # Specifies which retry strategy class to use. Strategy classes + # may have additional options, such as max_retries and backoff strategies. + # Available options are: + # * `Retry::Standard` - A standardized set of retry rules across the AWS SDKs. This includes support for retry quotas, which limit the number of unsuccessful retries a client can make. + # * `Retry::Adaptive` - An experimental retry mode that includes all the functionality of `standard` mode along with automatic client side throttling. This is a provisional mode that may change behavior in the future. # # @option args [Boolean] :stub_responses (false) # Enable response stubbing for testing. See {Hearth::ClientStubs stub_responses}. @@ -41,17 +37,14 @@ module Weather # @option args [Boolean] :validate_input (true) # When `true`, request parameters are validated using the modeled shapes. # - # @!attribute adaptive_retry_wait_to_fill - # @return [Boolean] - # # @!attribute disable_host_prefix # @return [Boolean] # # @!attribute endpoint # @return [String] # - # @!attribute http_wire_trace - # @return [Boolean] + # @!attribute http_client + # @return [Hearth::HTTP::Client] # # @!attribute log_level # @return [Symbol] @@ -59,11 +52,8 @@ module Weather # @!attribute logger # @return [Logger] # - # @!attribute max_attempts - # @return [Integer] - # - # @!attribute retry_mode - # @return [String] + # @!attribute retry_strategy + # @return [Hearth::Retry::Strategy] # # @!attribute stub_responses # @return [Boolean] @@ -72,14 +62,12 @@ module Weather # @return [Boolean] # Config = ::Struct.new( - :adaptive_retry_wait_to_fill, :disable_host_prefix, :endpoint, - :http_wire_trace, + :http_client, :log_level, :logger, - :max_attempts, - :retry_mode, + :retry_strategy, :stub_responses, :validate_input, keyword_init: true @@ -89,28 +77,24 @@ module Weather private def validate! - Hearth::Validator.validate_types!(adaptive_retry_wait_to_fill, TrueClass, FalseClass, context: 'options[:adaptive_retry_wait_to_fill]') Hearth::Validator.validate_types!(disable_host_prefix, TrueClass, FalseClass, context: 'options[:disable_host_prefix]') Hearth::Validator.validate_types!(endpoint, String, context: 'options[:endpoint]') - Hearth::Validator.validate_types!(http_wire_trace, TrueClass, FalseClass, context: 'options[:http_wire_trace]') + Hearth::Validator.validate_types!(http_client, Hearth::HTTP::Client, context: 'options[:http_client]') Hearth::Validator.validate_types!(log_level, Symbol, context: 'options[:log_level]') Hearth::Validator.validate_types!(logger, Logger, context: 'options[:logger]') - Hearth::Validator.validate_types!(max_attempts, Integer, context: 'options[:max_attempts]') - Hearth::Validator.validate_types!(retry_mode, String, context: 'options[:retry_mode]') + Hearth::Validator.validate_types!(retry_strategy, Hearth::Retry::Strategy, context: 'options[:retry_strategy]') Hearth::Validator.validate_types!(stub_responses, TrueClass, FalseClass, context: 'options[:stub_responses]') Hearth::Validator.validate_types!(validate_input, TrueClass, FalseClass, context: 'options[:validate_input]') end def self.defaults @defaults ||= { - adaptive_retry_wait_to_fill: [true], disable_host_prefix: [false], endpoint: [proc { |cfg| cfg[:stub_responses] ? 'http://localhost' : nil } ], - http_wire_trace: [false], + http_client: [proc { |cfg| Hearth::HTTP::Client.new(logger: cfg[:logger]) }], log_level: [:info], logger: [proc { |cfg| Logger.new($stdout, level: cfg[:log_level]) } ], - max_attempts: [3], - retry_mode: ['standard'], + retry_strategy: [Hearth::Retry::Standard.new], stub_responses: [false], validate_input: [true] }.freeze diff --git a/codegen/projections/weather/lib/weather/params.rb b/codegen/projections/weather/lib/weather/params.rb index c25decb5c..b63b56ad6 100644 --- a/codegen/projections/weather/lib/weather/params.rb +++ b/codegen/projections/weather/lib/weather/params.rb @@ -8,6 +8,7 @@ # WARNING ABOUT GENERATED CODE module Weather + # @api private module Params module Announcements diff --git a/codegen/projections/weather/lib/weather/parsers.rb b/codegen/projections/weather/lib/weather/parsers.rb index d8fbd2523..a5024262a 100644 --- a/codegen/projections/weather/lib/weather/parsers.rb +++ b/codegen/projections/weather/lib/weather/parsers.rb @@ -8,6 +8,7 @@ # WARNING ABOUT GENERATED CODE module Weather + # @api private module Parsers class Baz diff --git a/codegen/projections/weather/lib/weather/stubs.rb b/codegen/projections/weather/lib/weather/stubs.rb index b1d1acc34..7180fa288 100644 --- a/codegen/projections/weather/lib/weather/stubs.rb +++ b/codegen/projections/weather/lib/weather/stubs.rb @@ -8,6 +8,7 @@ # WARNING ABOUT GENERATED CODE module Weather + # @api private module Stubs # Union Stubber for Announcements diff --git a/codegen/projections/weather/lib/weather/types.rb b/codegen/projections/weather/lib/weather/types.rb index 56fbf3d36..c83b10327 100644 --- a/codegen/projections/weather/lib/weather/types.rb +++ b/codegen/projections/weather/lib/weather/types.rb @@ -584,6 +584,7 @@ module Resolution ULTRA = 3 end + # Includes enum constants for TypedYesNo # module TypedYesNo diff --git a/codegen/projections/weather/lib/weather/validators.rb b/codegen/projections/weather/lib/weather/validators.rb index efae20eae..8f97ab760 100644 --- a/codegen/projections/weather/lib/weather/validators.rb +++ b/codegen/projections/weather/lib/weather/validators.rb @@ -10,6 +10,7 @@ require 'time' module Weather + # @api private module Validators class Announcements diff --git a/codegen/projections/weather/spec/protocol_spec.rb b/codegen/projections/weather/spec/protocol_spec.rb index 5ad498059..42641b521 100644 --- a/codegen/projections/weather/spec/protocol_spec.rb +++ b/codegen/projections/weather/spec/protocol_spec.rb @@ -26,10 +26,11 @@ module Weather middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 404 - response.body = StringIO.new('{ + response.body.write('{ "resourceType": "City", "message": "Your custom message" }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -54,9 +55,8 @@ module Weather it 'WriteGetCityAssertions' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/cities/123') + expect(request.uri.path).to eq('/cities/123') expect(request.body.read).to eq('') Hearth::Output.new end @@ -74,7 +74,7 @@ module Weather middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.body = StringIO.new('{ + response.body.write('{ "name": "Seattle", "coordinates": { "latitude": 12.34, @@ -87,6 +87,7 @@ module Weather "case": "Upper" } }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -153,10 +154,11 @@ module Weather middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 404 - response.body = StringIO.new('{ + response.body.write('{ "resourceType": "City", "message": "Your custom message" }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -182,10 +184,11 @@ module Weather middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 404 - response.body = StringIO.new('{ + response.body.write('{ "resourceType": "City", "message": "Your custom message" }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -211,10 +214,11 @@ module Weather middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 404 - response.body = StringIO.new('{ + response.body.write('{ "resourceType": "City", "message": "Your custom message" }') + response.body.rewind Hearth::Output.new end middleware.remove_send.remove_build.remove_retry @@ -247,16 +251,15 @@ module Weather it 'WriteListCitiesAssertions' do middleware = Hearth::MiddlewareBuilder.before_send do |input, context| request = context.request - request_uri = URI.parse(request.url) expect(request.http_method).to eq('GET') - expect(request_uri.path).to eq('/cities') + expect(request.uri.path).to eq('/cities') expected_query = ::CGI.parse(['pageSize=50'].join('&')) - actual_query = ::CGI.parse(request_uri.query) + actual_query = ::CGI.parse(request.uri.query) expected_query.each do |k, v| expect(actual_query[k]).to eq(v) end forbid_query = ['nextToken'] - actual_query = ::CGI.parse(request_uri.query) + actual_query = ::CGI.parse(request.uri.query) forbid_query.each do |query| expect(actual_query.key?(query)).to be false end diff --git a/codegen/projections/white_label/lib/white_label/builders.rb b/codegen/projections/white_label/lib/white_label/builders.rb index 28b7406af..7ae949716 100644 --- a/codegen/projections/white_label/lib/white_label/builders.rb +++ b/codegen/projections/white_label/lib/white_label/builders.rb @@ -8,6 +8,7 @@ # WARNING ABOUT GENERATED CODE module WhiteLabel + # @api private module Builders # Operation Builder for DefaultsTest diff --git a/codegen/projections/white_label/lib/white_label/client.rb b/codegen/projections/white_label/lib/white_label/client.rb index 4d2ccac24..741c1dea4 100644 --- a/codegen/projections/white_label/lib/white_label/client.rb +++ b/codegen/projections/white_label/lib/white_label/client.rb @@ -48,8 +48,6 @@ def initialize(config = WhiteLabel::Config.new, options = {}) @config = config @middleware = Hearth::MiddlewareBuilder.new(options[:middleware]) @stubs = Hearth::Stubbing::Stubs.new - @retry_quota = Hearth::Retry::RetryQuota.new - @client_rate_limiter = Hearth::Retry::ClientRateLimiter.new end # @param [Hash] params @@ -133,12 +131,8 @@ def defaults_test(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -146,7 +140,7 @@ def defaults_test(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::DefaultsTest, stubs: @stubs, params_class: Params::DefaultsTestOutput @@ -156,7 +150,7 @@ def defaults_test(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -197,12 +191,8 @@ def endpoint_operation(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -210,7 +200,7 @@ def endpoint_operation(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::EndpointOperation, stubs: @stubs, params_class: Params::EndpointOperationOutput @@ -220,7 +210,7 @@ def endpoint_operation(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -263,12 +253,8 @@ def endpoint_with_host_label_operation(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -276,7 +262,7 @@ def endpoint_with_host_label_operation(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::EndpointWithHostLabelOperation, stubs: @stubs, params_class: Params::EndpointWithHostLabelOperationOutput @@ -286,7 +272,7 @@ def endpoint_with_host_label_operation(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -490,12 +476,8 @@ def kitchen_sink(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: [Errors::ClientError, Errors::ServerError]), @@ -503,7 +485,7 @@ def kitchen_sink(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::KitchenSink, stubs: @stubs, params_class: Params::KitchenSinkOutput @@ -513,7 +495,7 @@ def kitchen_sink(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -554,12 +536,8 @@ def mixin_test(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -567,7 +545,7 @@ def mixin_test(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::MixinTest, stubs: @stubs, params_class: Params::MixinTestOutput @@ -577,7 +555,7 @@ def mixin_test(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -619,12 +597,8 @@ def paginators_test(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -632,7 +606,7 @@ def paginators_test(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::PaginatorsTest, stubs: @stubs, params_class: Params::PaginatorsTestOperationOutput @@ -642,7 +616,7 @@ def paginators_test(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -684,12 +658,8 @@ def paginators_test_with_items(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -697,7 +667,7 @@ def paginators_test_with_items(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::PaginatorsTestWithItems, stubs: @stubs, params_class: Params::PaginatorsTestWithItemsOutput @@ -707,7 +677,7 @@ def paginators_test_with_items(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -746,12 +716,8 @@ def streaming_operation(params = {}, options = {}, &block) builder: Builders::StreamingOperation ) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -759,7 +725,7 @@ def streaming_operation(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::StreamingOperation, stubs: @stubs, params_class: Params::StreamingOperationOutput @@ -769,7 +735,7 @@ def streaming_operation(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -808,12 +774,8 @@ def streaming_with_length(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -821,7 +783,7 @@ def streaming_with_length(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::StreamingWithLength, stubs: @stubs, params_class: Params::StreamingWithLengthOutput @@ -831,7 +793,7 @@ def streaming_with_length(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -871,12 +833,8 @@ def waiters_test(params = {}, options = {}, &block) ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -884,7 +842,7 @@ def waiters_test(params = {}, options = {}, &block) ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::WaitersTest, stubs: @stubs, params_class: Params::WaitersTestOutput @@ -894,7 +852,7 @@ def waiters_test(params = {}, options = {}, &block) resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, @@ -937,12 +895,8 @@ def operation____paginators_test_with_bad_names(params = {}, options = {}, &bloc ) stack.use(Hearth::HTTP::Middleware::ContentLength) stack.use(Hearth::Middleware::Retry, - retry_mode: @config.retry_mode, - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: @retry_quota, - max_attempts: @config.max_attempts, - client_rate_limiter: @client_rate_limiter, - adaptive_retry_wait_to_fill: @config.adaptive_retry_wait_to_fill + retry_strategy: @config.retry_strategy, + error_inspector_class: Hearth::HTTP::ErrorInspector ) stack.use(Hearth::Middleware::Parse, error_parser: Hearth::HTTP::ErrorParser.new(error_module: Errors, success_status: 200, errors: []), @@ -950,7 +904,7 @@ def operation____paginators_test_with_bad_names(params = {}, options = {}, &bloc ) stack.use(Hearth::Middleware::Send, stub_responses: @config.stub_responses, - client: Hearth::HTTP::Client.new(logger: @config.logger, http_wire_trace: options.fetch(:http_wire_trace, @config.http_wire_trace)), + client: options.fetch(:http_client, @config.http_client), stub_class: Stubs::Operation____PaginatorsTestWithBadNames, stubs: @stubs, params_class: Params::Struct____PaginatorsTestWithBadNamesOutput @@ -960,7 +914,7 @@ def operation____paginators_test_with_bad_names(params = {}, options = {}, &bloc resp = stack.run( input: input, context: Hearth::Context.new( - request: Hearth::HTTP::Request.new(url: options.fetch(:endpoint, @config.endpoint)), + request: Hearth::HTTP::Request.new(uri: URI(options.fetch(:endpoint, @config.endpoint))), response: Hearth::HTTP::Response.new(body: response_body), params: params, logger: @config.logger, diff --git a/codegen/projections/white_label/lib/white_label/config.rb b/codegen/projections/white_label/lib/white_label/config.rb index 3fbb9e45e..90abffd38 100644 --- a/codegen/projections/white_label/lib/white_label/config.rb +++ b/codegen/projections/white_label/lib/white_label/config.rb @@ -9,31 +9,27 @@ module WhiteLabel # @!method initialize(*options) - # @option args [Boolean] :adaptive_retry_wait_to_fill (true) - # Used only in `adaptive` retry mode. When true, the request will sleep until there is sufficient client side capacity to retry the request. When false, the request will raise a `CapacityNotAvailableError` and will not retry instead of sleeping. - # # @option args [Boolean] :disable_host_prefix (false) # When `true`, does not perform host prefix injection using @endpoint's hostPrefix property. # # @option args [String] :endpoint # Endpoint of the service # - # @option args [Boolean] :http_wire_trace (false) - # Enable debug wire trace on http requests. + # @option args [Hearth::HTTP::Client] :http_client (Hearth::HTTP::Client.new) + # The HTTP Client to use for request transport. # # @option args [Symbol] :log_level (:info) - # Default log level to use - # - # @option args [Logger] :logger ($stdout) - # Logger to use for output + # The default log level to use with the Logger. # - # @option args [Integer] :max_attempts (3) - # An integer representing the maximum number of attempts that will be made for a single request, including the initial attempt. + # @option args [Logger] :logger (Logger.new($stdout, level: cfg.log_level)) + # The Logger instance to use for logging. # - # @option args [String] :retry_mode ('standard') - # Specifies which retry algorithm to use. Values are: - # * `standard` - A standardized set of retry rules across the AWS SDKs. This includes support for retry quotas, which limit the number of unsuccessful retries a client can make. - # * `adaptive` - An experimental retry mode that includes all the functionality of `standard` mode along with automatic client side throttling. This is a provisional mode that may change behavior in the future. + # @option args [Hearth::Retry::Strategy] :retry_strategy (Hearth::Retry::Standard.new) + # Specifies which retry strategy class to use. Strategy classes + # may have additional options, such as max_retries and backoff strategies. + # Available options are: + # * `Retry::Standard` - A standardized set of retry rules across the AWS SDKs. This includes support for retry quotas, which limit the number of unsuccessful retries a client can make. + # * `Retry::Adaptive` - An experimental retry mode that includes all the functionality of `standard` mode along with automatic client side throttling. This is a provisional mode that may change behavior in the future. # # @option args [Boolean] :stub_responses (false) # Enable response stubbing for testing. See {Hearth::ClientStubs stub_responses}. @@ -41,17 +37,14 @@ module WhiteLabel # @option args [Boolean] :validate_input (true) # When `true`, request parameters are validated using the modeled shapes. # - # @!attribute adaptive_retry_wait_to_fill - # @return [Boolean] - # # @!attribute disable_host_prefix # @return [Boolean] # # @!attribute endpoint # @return [String] # - # @!attribute http_wire_trace - # @return [Boolean] + # @!attribute http_client + # @return [Hearth::HTTP::Client] # # @!attribute log_level # @return [Symbol] @@ -59,11 +52,8 @@ module WhiteLabel # @!attribute logger # @return [Logger] # - # @!attribute max_attempts - # @return [Integer] - # - # @!attribute retry_mode - # @return [String] + # @!attribute retry_strategy + # @return [Hearth::Retry::Strategy] # # @!attribute stub_responses # @return [Boolean] @@ -72,14 +62,12 @@ module WhiteLabel # @return [Boolean] # Config = ::Struct.new( - :adaptive_retry_wait_to_fill, :disable_host_prefix, :endpoint, - :http_wire_trace, + :http_client, :log_level, :logger, - :max_attempts, - :retry_mode, + :retry_strategy, :stub_responses, :validate_input, keyword_init: true @@ -89,28 +77,24 @@ module WhiteLabel private def validate! - Hearth::Validator.validate_types!(adaptive_retry_wait_to_fill, TrueClass, FalseClass, context: 'options[:adaptive_retry_wait_to_fill]') Hearth::Validator.validate_types!(disable_host_prefix, TrueClass, FalseClass, context: 'options[:disable_host_prefix]') Hearth::Validator.validate_types!(endpoint, String, context: 'options[:endpoint]') - Hearth::Validator.validate_types!(http_wire_trace, TrueClass, FalseClass, context: 'options[:http_wire_trace]') + Hearth::Validator.validate_types!(http_client, Hearth::HTTP::Client, context: 'options[:http_client]') Hearth::Validator.validate_types!(log_level, Symbol, context: 'options[:log_level]') Hearth::Validator.validate_types!(logger, Logger, context: 'options[:logger]') - Hearth::Validator.validate_types!(max_attempts, Integer, context: 'options[:max_attempts]') - Hearth::Validator.validate_types!(retry_mode, String, context: 'options[:retry_mode]') + Hearth::Validator.validate_types!(retry_strategy, Hearth::Retry::Strategy, context: 'options[:retry_strategy]') Hearth::Validator.validate_types!(stub_responses, TrueClass, FalseClass, context: 'options[:stub_responses]') Hearth::Validator.validate_types!(validate_input, TrueClass, FalseClass, context: 'options[:validate_input]') end def self.defaults @defaults ||= { - adaptive_retry_wait_to_fill: [true], disable_host_prefix: [false], endpoint: [proc { |cfg| cfg[:stub_responses] ? 'http://localhost' : nil } ], - http_wire_trace: [false], + http_client: [proc { |cfg| Hearth::HTTP::Client.new(logger: cfg[:logger]) }], log_level: [:info], logger: [proc { |cfg| Logger.new($stdout, level: cfg[:log_level]) } ], - max_attempts: [3], - retry_mode: ['standard'], + retry_strategy: [Hearth::Retry::Standard.new], stub_responses: [false], validate_input: [true] }.freeze diff --git a/codegen/projections/white_label/lib/white_label/params.rb b/codegen/projections/white_label/lib/white_label/params.rb index 0e92ea6f8..93e60856a 100644 --- a/codegen/projections/white_label/lib/white_label/params.rb +++ b/codegen/projections/white_label/lib/white_label/params.rb @@ -8,6 +8,7 @@ # WARNING ABOUT GENERATED CODE module WhiteLabel + # @api private module Params module ClientError @@ -33,7 +34,7 @@ def self.build(params, context: '') type.simple_enum = params.fetch(:simple_enum, "YES") type.typed_enum = params.fetch(:typed_enum, "NO") type.int_enum = params.fetch(:int_enum, 1) - type.null_document = params.fetch(:null_document, nil) + type.null_document = params[:null_document] type.string_document = params.fetch(:string_document, "some string document") type.boolean_document = params.fetch(:boolean_document, true) type.numbers_document = params.fetch(:numbers_document, 1.23) @@ -61,7 +62,7 @@ def self.build(params, context: '') type.simple_enum = params.fetch(:simple_enum, "YES") type.typed_enum = params.fetch(:typed_enum, "NO") type.int_enum = params.fetch(:int_enum, 1) - type.null_document = params.fetch(:null_document, nil) + type.null_document = params[:null_document] type.string_document = params.fetch(:string_document, "some string document") type.boolean_document = params.fetch(:boolean_document, true) type.numbers_document = params.fetch(:numbers_document, 1.23) diff --git a/codegen/projections/white_label/lib/white_label/parsers.rb b/codegen/projections/white_label/lib/white_label/parsers.rb index 3881f9976..491975939 100644 --- a/codegen/projections/white_label/lib/white_label/parsers.rb +++ b/codegen/projections/white_label/lib/white_label/parsers.rb @@ -8,6 +8,7 @@ # WARNING ABOUT GENERATED CODE module WhiteLabel + # @api private module Parsers # Error Parser for ClientError diff --git a/codegen/projections/white_label/lib/white_label/stubs.rb b/codegen/projections/white_label/lib/white_label/stubs.rb index d9ac06816..85861260d 100644 --- a/codegen/projections/white_label/lib/white_label/stubs.rb +++ b/codegen/projections/white_label/lib/white_label/stubs.rb @@ -8,6 +8,7 @@ # WARNING ABOUT GENERATED CODE module WhiteLabel + # @api private module Stubs # Operation Stubber for DefaultsTest diff --git a/codegen/projections/white_label/lib/white_label/validators.rb b/codegen/projections/white_label/lib/white_label/validators.rb index b5f6c4783..55c1f3624 100644 --- a/codegen/projections/white_label/lib/white_label/validators.rb +++ b/codegen/projections/white_label/lib/white_label/validators.rb @@ -10,6 +10,7 @@ require 'time' module WhiteLabel + # @api private module Validators class ClientError diff --git a/codegen/projections/white_label/spec/client_spec.rb b/codegen/projections/white_label/spec/client_spec.rb index 640d3c264..eac22740a 100644 --- a/codegen/projections/white_label/spec/client_spec.rb +++ b/codegen/projections/white_label/spec/client_spec.rb @@ -17,27 +17,18 @@ module WhiteLabel client.kitchen_sink end - it 'uses retry_mode, max_attempts, and adaptive_retry_wait_to_fill' do + it 'uses retry_strategy' do expect(Hearth::Middleware::Retry) .to receive(:new) .with(anything, - retry_mode: config.retry_mode, - max_attempts: config.max_attempts, - adaptive_retry_wait_to_fill: config.adaptive_retry_wait_to_fill, - error_inspector_class: anything, - client_rate_limiter: anything, - retry_quota: anything) + retry_strategy: config.retry_strategy, + error_inspector_class: anything) .and_call_original client.kitchen_sink end it 'uses logger' do - expect(Hearth::HTTP::Client) - .to receive(:new) - .with(hash_including(logger: config.logger)) - .and_call_original - expect(Hearth::Context) .to receive(:new) .with(hash_including(logger: config.logger)) @@ -46,28 +37,10 @@ module WhiteLabel client.kitchen_sink end - it 'uses http_wire_trace from config' do - expect(Hearth::HTTP::Client) - .to receive(:new) - .with(hash_including(http_wire_trace: config.http_wire_trace)) - .and_call_original - - client.kitchen_sink - end - - it 'uses http_wire_trace from options' do - expect(Hearth::HTTP::Client) - .to receive(:new) - .with(hash_including(http_wire_trace: true)) - .and_call_original - - client.kitchen_sink({}, http_wire_trace: true) - end - it 'uses endpoint from config' do expect(Hearth::HTTP::Request) .to receive(:new) - .with(hash_including(url: config.endpoint)) + .with(hash_including(uri: URI(config.endpoint))) .and_call_original client.kitchen_sink @@ -76,7 +49,7 @@ module WhiteLabel it 'uses endpoint from options' do expect(Hearth::HTTP::Request) .to receive(:new) - .with(hash_including(url: 'endpoint')) + .with(hash_including(uri: URI('endpoint'))) .and_call_original client.kitchen_sink({}, endpoint: 'endpoint') diff --git a/codegen/projections/white_label/spec/config_spec.rb b/codegen/projections/white_label/spec/config_spec.rb index 8806a1169..9bfe7302a 100644 --- a/codegen/projections/white_label/spec/config_spec.rb +++ b/codegen/projections/white_label/spec/config_spec.rb @@ -7,14 +7,12 @@ module WhiteLabel describe '#build' do it 'sets member values' do config_keys = { - adaptive_retry_wait_to_fill: false, disable_host_prefix: true, endpoint: 'test', - http_wire_trace: true, + http_client: Hearth::HTTP::Client.new, log_level: :debug, logger: Logger.new($stdout, level: :debug), - max_attempts: 0, - retry_mode: 'adaptive', + retry_strategy: Hearth::Retry::Adaptive.new, stub_responses: false, validate_input: false } diff --git a/codegen/projections/white_label/spec/endpoints_spec.rb b/codegen/projections/white_label/spec/endpoints_spec.rb index 6ecc1afa3..b5b2609ab 100644 --- a/codegen/projections/white_label/spec/endpoints_spec.rb +++ b/codegen/projections/white_label/spec/endpoints_spec.rb @@ -9,7 +9,7 @@ module WhiteLabel describe '#endpoint_operation' do it 'prepends to the host' do middleware = Hearth::MiddlewareBuilder.before_send do |_, context| - expect(context.request.url).to include('foo') + expect(context.request.uri.to_s).to include('foo') end client.endpoint_operation( {}, middleware: middleware) end @@ -28,7 +28,7 @@ module WhiteLabel it 'prepends the label to the host' do middleware = Hearth::MiddlewareBuilder.before_send do |_, context| - expect(context.request.url).to include("foo.#{label}") + expect(context.request.uri.to_s).to include("foo.#{label}") end client.endpoint_with_host_label_operation({ label_member: label }, middleware: middleware) end diff --git a/codegen/projections/white_label/spec/errors_spec.rb b/codegen/projections/white_label/spec/errors_spec.rb index 10c92e75d..f93dcd685 100644 --- a/codegen/projections/white_label/spec/errors_spec.rb +++ b/codegen/projections/white_label/spec/errors_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require_relative 'spec_helper' module WhiteLabel module Errors diff --git a/codegen/projections/white_label/spec/retry_spec.rb b/codegen/projections/white_label/spec/retry_spec.rb index da933cb08..cbcf41f2b 100644 --- a/codegen/projections/white_label/spec/retry_spec.rb +++ b/codegen/projections/white_label/spec/retry_spec.rb @@ -15,8 +15,10 @@ module WhiteLabel # first return error, then some data client.stub_responses(:kitchen_sink, [error, { string: "ok" }]) + expect_any_instance_of(Hearth::Retry::Standard).to receive(:acquire_initial_retry_token).and_call_original + expect_any_instance_of(Hearth::Retry::Standard).to receive(:refresh_retry_token).and_call_original + expect_any_instance_of(Hearth::Retry::Standard).to receive(:record_success).and_call_original expect(Kernel).to receive(:sleep).once - expect_any_instance_of(Hearth::Middleware::Retry).to receive(:call).twice.and_call_original client.kitchen_sink end diff --git a/codegen/projections/white_label/spec/streaming_spec.rb b/codegen/projections/white_label/spec/streaming_spec.rb index 06efb42a0..b4e40c63e 100644 --- a/codegen/projections/white_label/spec/streaming_spec.rb +++ b/codegen/projections/white_label/spec/streaming_spec.rb @@ -96,7 +96,7 @@ module WhiteLabel streaming_input = StringIO.new("test") middleware = Hearth::MiddlewareBuilder.before_send do |_, context| expect(context.request.headers['Transfer-Encoding']).to eq('chunked') - expect(context.request.headers.key?('Content-Length')).to be_falsy + expect(context.request.fields.key?('Content-Length')).to eq(false) end expect(streaming_input).not_to receive(:size) client.streaming_operation({stream: streaming_input}, middleware: middleware) @@ -110,7 +110,7 @@ module WhiteLabel it 'sets content-length and does not set Transfer-Encoding' do middleware = Hearth::MiddlewareBuilder.before_send do |_, context| expect(context.request.headers['Content-Length']).to eq(data.length.to_s) - expect(context.request.headers.key?('Transfer-Encoding')).to be_falsy + expect(context.request.fields.key?('Transfer-Encoding')).to eq(false) end client.streaming_with_length({stream: data}, middleware: middleware) end diff --git a/codegen/smithy-ruby-codegen-test/integration-specs/client_spec.rb b/codegen/smithy-ruby-codegen-test/integration-specs/client_spec.rb index 640d3c264..eac22740a 100644 --- a/codegen/smithy-ruby-codegen-test/integration-specs/client_spec.rb +++ b/codegen/smithy-ruby-codegen-test/integration-specs/client_spec.rb @@ -17,27 +17,18 @@ module WhiteLabel client.kitchen_sink end - it 'uses retry_mode, max_attempts, and adaptive_retry_wait_to_fill' do + it 'uses retry_strategy' do expect(Hearth::Middleware::Retry) .to receive(:new) .with(anything, - retry_mode: config.retry_mode, - max_attempts: config.max_attempts, - adaptive_retry_wait_to_fill: config.adaptive_retry_wait_to_fill, - error_inspector_class: anything, - client_rate_limiter: anything, - retry_quota: anything) + retry_strategy: config.retry_strategy, + error_inspector_class: anything) .and_call_original client.kitchen_sink end it 'uses logger' do - expect(Hearth::HTTP::Client) - .to receive(:new) - .with(hash_including(logger: config.logger)) - .and_call_original - expect(Hearth::Context) .to receive(:new) .with(hash_including(logger: config.logger)) @@ -46,28 +37,10 @@ module WhiteLabel client.kitchen_sink end - it 'uses http_wire_trace from config' do - expect(Hearth::HTTP::Client) - .to receive(:new) - .with(hash_including(http_wire_trace: config.http_wire_trace)) - .and_call_original - - client.kitchen_sink - end - - it 'uses http_wire_trace from options' do - expect(Hearth::HTTP::Client) - .to receive(:new) - .with(hash_including(http_wire_trace: true)) - .and_call_original - - client.kitchen_sink({}, http_wire_trace: true) - end - it 'uses endpoint from config' do expect(Hearth::HTTP::Request) .to receive(:new) - .with(hash_including(url: config.endpoint)) + .with(hash_including(uri: URI(config.endpoint))) .and_call_original client.kitchen_sink @@ -76,7 +49,7 @@ module WhiteLabel it 'uses endpoint from options' do expect(Hearth::HTTP::Request) .to receive(:new) - .with(hash_including(url: 'endpoint')) + .with(hash_including(uri: URI('endpoint'))) .and_call_original client.kitchen_sink({}, endpoint: 'endpoint') diff --git a/codegen/smithy-ruby-codegen-test/integration-specs/config_spec.rb b/codegen/smithy-ruby-codegen-test/integration-specs/config_spec.rb index 8806a1169..9bfe7302a 100644 --- a/codegen/smithy-ruby-codegen-test/integration-specs/config_spec.rb +++ b/codegen/smithy-ruby-codegen-test/integration-specs/config_spec.rb @@ -7,14 +7,12 @@ module WhiteLabel describe '#build' do it 'sets member values' do config_keys = { - adaptive_retry_wait_to_fill: false, disable_host_prefix: true, endpoint: 'test', - http_wire_trace: true, + http_client: Hearth::HTTP::Client.new, log_level: :debug, logger: Logger.new($stdout, level: :debug), - max_attempts: 0, - retry_mode: 'adaptive', + retry_strategy: Hearth::Retry::Adaptive.new, stub_responses: false, validate_input: false } diff --git a/codegen/smithy-ruby-codegen-test/integration-specs/endpoints_spec.rb b/codegen/smithy-ruby-codegen-test/integration-specs/endpoints_spec.rb index 6ecc1afa3..b5b2609ab 100644 --- a/codegen/smithy-ruby-codegen-test/integration-specs/endpoints_spec.rb +++ b/codegen/smithy-ruby-codegen-test/integration-specs/endpoints_spec.rb @@ -9,7 +9,7 @@ module WhiteLabel describe '#endpoint_operation' do it 'prepends to the host' do middleware = Hearth::MiddlewareBuilder.before_send do |_, context| - expect(context.request.url).to include('foo') + expect(context.request.uri.to_s).to include('foo') end client.endpoint_operation( {}, middleware: middleware) end @@ -28,7 +28,7 @@ module WhiteLabel it 'prepends the label to the host' do middleware = Hearth::MiddlewareBuilder.before_send do |_, context| - expect(context.request.url).to include("foo.#{label}") + expect(context.request.uri.to_s).to include("foo.#{label}") end client.endpoint_with_host_label_operation({ label_member: label }, middleware: middleware) end diff --git a/codegen/smithy-ruby-codegen-test/integration-specs/errors_spec.rb b/codegen/smithy-ruby-codegen-test/integration-specs/errors_spec.rb index 10c92e75d..f93dcd685 100644 --- a/codegen/smithy-ruby-codegen-test/integration-specs/errors_spec.rb +++ b/codegen/smithy-ruby-codegen-test/integration-specs/errors_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require_relative 'spec_helper' module WhiteLabel module Errors diff --git a/codegen/smithy-ruby-codegen-test/integration-specs/retry_spec.rb b/codegen/smithy-ruby-codegen-test/integration-specs/retry_spec.rb index da933cb08..cbcf41f2b 100644 --- a/codegen/smithy-ruby-codegen-test/integration-specs/retry_spec.rb +++ b/codegen/smithy-ruby-codegen-test/integration-specs/retry_spec.rb @@ -15,8 +15,10 @@ module WhiteLabel # first return error, then some data client.stub_responses(:kitchen_sink, [error, { string: "ok" }]) + expect_any_instance_of(Hearth::Retry::Standard).to receive(:acquire_initial_retry_token).and_call_original + expect_any_instance_of(Hearth::Retry::Standard).to receive(:refresh_retry_token).and_call_original + expect_any_instance_of(Hearth::Retry::Standard).to receive(:record_success).and_call_original expect(Kernel).to receive(:sleep).once - expect_any_instance_of(Hearth::Middleware::Retry).to receive(:call).twice.and_call_original client.kitchen_sink end diff --git a/codegen/smithy-ruby-codegen-test/integration-specs/streaming_spec.rb b/codegen/smithy-ruby-codegen-test/integration-specs/streaming_spec.rb index 06efb42a0..b4e40c63e 100644 --- a/codegen/smithy-ruby-codegen-test/integration-specs/streaming_spec.rb +++ b/codegen/smithy-ruby-codegen-test/integration-specs/streaming_spec.rb @@ -96,7 +96,7 @@ module WhiteLabel streaming_input = StringIO.new("test") middleware = Hearth::MiddlewareBuilder.before_send do |_, context| expect(context.request.headers['Transfer-Encoding']).to eq('chunked') - expect(context.request.headers.key?('Content-Length')).to be_falsy + expect(context.request.fields.key?('Content-Length')).to eq(false) end expect(streaming_input).not_to receive(:size) client.streaming_operation({stream: streaming_input}, middleware: middleware) @@ -110,7 +110,7 @@ module WhiteLabel it 'sets content-length and does not set Transfer-Encoding' do middleware = Hearth::MiddlewareBuilder.before_send do |_, context| expect(context.request.headers['Content-Length']).to eq(data.length.to_s) - expect(context.request.headers.key?('Transfer-Encoding')).to be_falsy + expect(context.request.fields.key?('Transfer-Encoding')).to eq(false) end client.streaming_with_length({stream: data}, middleware: middleware) end diff --git a/codegen/smithy-ruby-codegen-test/model/weather.smithy b/codegen/smithy-ruby-codegen-test/model/weather.smithy index feb35ffc2..2cdd125b1 100644 --- a/codegen/smithy-ruby-codegen-test/model/weather.smithy +++ b/codegen/smithy-ruby-codegen-test/model/weather.smithy @@ -400,8 +400,10 @@ structure OtherStructure {} @enum([{value: "YES"}, {value: "NO"}]) string SimpleYesNo -@enum([{value: "YES", name: "YES"}, {value: "NO", name: "NO"}]) -string TypedYesNo +enum TypedYesNo { + YES = "YES" + NO = "NO" +} map StringMap { key: String, diff --git a/codegen/smithy-ruby-codegen/build.gradle.kts b/codegen/smithy-ruby-codegen/build.gradle.kts index 0d1d7ef3a..ac5e6c225 100644 --- a/codegen/smithy-ruby-codegen/build.gradle.kts +++ b/codegen/smithy-ruby-codegen/build.gradle.kts @@ -33,6 +33,7 @@ buildscript { dependencies { api("software.amazon.smithy:smithy-codegen-core:${rootProject.extra["smithyVersion"]}") + api("software.amazon.smithy:smithy-rules-engine:${rootProject.extra["smithyVersion"]}") implementation("software.amazon.smithy:smithy-waiters:${rootProject.extra["smithyVersion"]}") implementation("software.amazon.smithy:smithy-protocol-test-traits:${rootProject.extra["smithyVersion"]}") } diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/ApplicationTransport.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/ApplicationTransport.java index 65b33fcf0..7a4898409 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/ApplicationTransport.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/ApplicationTransport.java @@ -93,46 +93,29 @@ public static ApplicationTransport createDefaultHttpApplicationTransport() { ClientFragment request = (new ClientFragment.Builder()) .addConfig(endpoint) - .render((self, ctx) -> "Hearth::HTTP::Request.new(url: " + endpoint.renderGetConfigValue() + ")") + // TODO: Replace URI with Endpoint middleware - should be a blank request + .render((self, ctx) -> "Hearth::HTTP::Request.new(uri: URI(" + endpoint.renderGetConfigValue() + "))") .build(); ClientFragment response = (new ClientFragment.Builder()) .render((self, ctx) -> "Hearth::HTTP::Response.new(body: response_body)") .build(); - ClientConfig wireTrace = (new ClientConfig.Builder()) - .name("http_wire_trace") - .type("Boolean") - .defaultValue("false") - .documentation("Enable debug wire trace on http requests.") + ClientConfig httpClient = (new ClientConfig.Builder()) + .name("http_client") + .type("Hearth::HTTP::Client") + .documentation("The HTTP Client to use for request transport.") + .documentationDefaultValue("Hearth::HTTP::Client.new") .allowOperationOverride() - .build(); - - ClientConfig logger = (new ClientConfig.Builder()) - .name("logger") - .type("Logger") - .documentationDefaultValue("$stdout") .defaults(new ConfigProviderChain.Builder() - .dynamicProvider("proc { |cfg| Logger.new($stdout, level: cfg[:log_level]) } ") + .dynamicProvider("proc { |cfg| Hearth::HTTP::Client.new(logger: cfg[:logger]) }") .build() ) - .documentation("Logger to use for output") - .build(); - - ClientConfig logLevel = (new ClientConfig.Builder()) - .name("log_level") - .type("Symbol") - .defaultValue(":info") - .documentation("Default log level to use") .build(); ClientFragment client = (new ClientFragment.Builder()) - .addConfig(wireTrace) - .addConfig(logger) - .addConfig(logLevel) - .render((self, ctx) -> "Hearth::HTTP::Client.new(logger: " + logger.renderGetConfigValue() - + ", http_wire_trace: " - + wireTrace.renderGetConfigValue() + ")") + .addConfig(httpClient) + .render((self, ctx) -> httpClient.renderGetConfigValue()) .build(); MiddlewareList defaultMiddleware = (transport, context) -> { @@ -154,12 +137,8 @@ public static ApplicationTransport createDefaultHttpApplicationTransport() { .klass("Hearth::HTTP::Middleware::ContentLength") .operationPredicate( (model, service, operation) -> - !Streaming.isNonFiniteStreaming(model, - model.expectShape( - operation.getInputShape(), - StructureShape.class - ) - ) + !Streaming.isNonFiniteStreaming( + model, model.expectShape(operation.getInputShape(), StructureShape.class)) ) .step(MiddlewareStackStep.BUILD) .build() @@ -253,6 +232,13 @@ public ClientFragment getTransportClient() { return transportClient; } + /** + * @return the error inspector used for HTTP errors. + */ + public String getErrorInspector() { + return Hearth.HTTP_ERROR_INSPECTOR.toString(); + } + /** * @param context generation context * @return list of default middleware to support this transport. diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/CodegenUtils.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/CodegenUtils.java index 65705d3f6..aa6995111 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/CodegenUtils.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/CodegenUtils.java @@ -61,6 +61,6 @@ public static boolean isStubSyntheticClone(Shape shape) { * @return TreeSet sorted by shape name in alphabetical order */ public static TreeSet getAlphabeticalOrderedShapesSet() { - return new TreeSet<>(Comparator.comparing(o -> o.getId().getName())); + return new TreeSet<>(Comparator.comparing(o -> o.getId().getName() + " " + o.getId())); } } diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/DirectedRubyCodegen.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/DirectedRubyCodegen.java index 397ef1826..b837b052e 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/DirectedRubyCodegen.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/DirectedRubyCodegen.java @@ -15,7 +15,6 @@ package software.amazon.smithy.ruby.codegen; -import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashSet; @@ -38,17 +37,20 @@ import software.amazon.smithy.codegen.core.directed.GenerateUnionDirective; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.ServiceShape; -import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.ruby.codegen.config.ClientConfig; import software.amazon.smithy.ruby.codegen.generators.ClientGenerator; import software.amazon.smithy.ruby.codegen.generators.ConfigGenerator; +import software.amazon.smithy.ruby.codegen.generators.EnumGenerator; import software.amazon.smithy.ruby.codegen.generators.GemspecGenerator; import software.amazon.smithy.ruby.codegen.generators.HttpProtocolTestGenerator; +import software.amazon.smithy.ruby.codegen.generators.IntEnumGenerator; import software.amazon.smithy.ruby.codegen.generators.ModuleGenerator; import software.amazon.smithy.ruby.codegen.generators.PaginatorsGenerator; import software.amazon.smithy.ruby.codegen.generators.ParamsGenerator; -import software.amazon.smithy.ruby.codegen.generators.TypesGenerator; +import software.amazon.smithy.ruby.codegen.generators.StructureGenerator; +import software.amazon.smithy.ruby.codegen.generators.TypesFileBlockGenerator; +import software.amazon.smithy.ruby.codegen.generators.UnionGenerator; import software.amazon.smithy.ruby.codegen.generators.ValidatorsGenerator; import software.amazon.smithy.ruby.codegen.generators.WaitersGenerator; import software.amazon.smithy.ruby.codegen.generators.YardOptsGenerator; @@ -60,7 +62,7 @@ public class DirectedRubyCodegen private static final Logger LOGGER = Logger.getLogger(DirectedRubyCodegen.class.getName()); - private List typeShapes = new ArrayList<>(); + private TypesFileBlockGenerator typesFileBlockGenerator; @Override public SymbolProvider createSymbolProvider(CreateSymbolProviderDirective directive) { @@ -71,7 +73,10 @@ public SymbolProvider createSymbolProvider(CreateSymbolProviderDirective directive) { ServiceShape service = directive.service(); Model model = directive.model(); - List integrations = directive.integrations(); + List integrations = directive.integrations().stream() + .filter((integration) -> integration + .includeFor(service, model)) + .collect(Collectors.toList()); Map supportedProtocols = ProtocolGenerator .collectSupportedProtocolGenerators(integrations); @@ -110,7 +115,6 @@ public GenerationContext createContext(CreateContextDirective directive) { GenerationContext context = directive.context(); - // Register all middleware MiddlewareBuilder middlewareBuilder = new MiddlewareBuilder(); middlewareBuilder.addDefaultMiddleware(context); @@ -140,20 +144,32 @@ public void generateService(GenerateServiceDirective m.toString()).collect(Collectors.joining(","))); - ConfigGenerator configGenerator = new ConfigGenerator(context); - configGenerator.render(clientConfigList); + ConfigGenerator configGenerator = new ConfigGenerator(directive, clientConfigList); + configGenerator.render(); configGenerator.renderRbs(); LOGGER.info("generated config"); - ClientGenerator clientGenerator = new ClientGenerator(context); - clientGenerator.render(middlewareBuilder); + ClientGenerator clientGenerator = new ClientGenerator(directive, middlewareBuilder); + clientGenerator.render(); clientGenerator.renderRbs(); LOGGER.info("generated client"); + + WaitersGenerator waitersGenerator = new WaitersGenerator(directive); + waitersGenerator.render(); + waitersGenerator.renderRbs(); + } + + @Override + public void customizeBeforeShapeGeneration(CustomizeDirective directive) { + this.typesFileBlockGenerator = new TypesFileBlockGenerator(directive); + + // Pre-populate module blocks for types.rb and types.rbs files + this.typesFileBlockGenerator.openBlocks(); } @Override public void generateStructure(GenerateStructureDirective directive) { - typeShapes.add(directive.shape()); + new StructureGenerator(directive).render(); } @Override @@ -161,54 +177,29 @@ public void generateError(GenerateErrorDirective directive) { - typeShapes.add(directive.shape()); + new UnionGenerator(directive).render(); } @Override public void generateEnumShape(GenerateEnumDirective directive) { - typeShapes.add(directive.shape()); + new EnumGenerator(directive).render(); } @Override public void generateIntEnumShape(GenerateIntEnumDirective directive) { - typeShapes.add(directive.shape()); + new IntEnumGenerator(directive).render(); } @Override public void customizeBeforeIntegrations(CustomizeDirective directive) { GenerationContext context = directive.context(); - - // Generate types - TypesGenerator typesGenerator = new TypesGenerator(context); - context.writerDelegator().useFileWriter( - typesGenerator.getFile(), typesGenerator.getNameSpace(), writer -> { - writer.includePreamble().includeRequires(); - - TypesGenerator.TypesVisitor visitor = typesGenerator.getTypeVisitor(writer); - - writer.addModule(directive.settings().getModule()); - writer.addModule("Types"); - - typeShapes.stream() - .sorted(Comparator.comparing(a -> a.getId().getName())) - .forEach(shape -> shape.accept(visitor)); - - writer.closeAllModules(); - }); - - typesGenerator.renderRbs(); - - ParamsGenerator paramsGenerator = new ParamsGenerator(context); - paramsGenerator.render(); - - ValidatorsGenerator validatorsGenerator = new ValidatorsGenerator(context); - validatorsGenerator.render(); - + new ParamsGenerator(directive).render(); + new ValidatorsGenerator(directive).render(); if (directive.context().protocolGenerator().isPresent()) { ProtocolGenerator generator = directive.context().protocolGenerator().get(); @@ -217,20 +208,11 @@ public void customizeBeforeIntegrations(CustomizeDirective additionalFiles = context.integrations().stream() - .map((integration) -> integration.writeAdditionalFiles(context)) - .flatMap(Collection::stream) - .collect(Collectors.toList()); - - new ModuleGenerator(context).render(additionalFiles); + new ModuleGenerator(directive).render(); new GemspecGenerator(context).render(); new YardOptsGenerator(context).render(); @@ -241,6 +223,12 @@ public void customizeBeforeIntegrations(CustomizeDirective directive) { + // Close all module blocks for types.rb and types.rbs files + this.typesFileBlockGenerator.closeAllBlocks(); + } + private Set collectDependencies( Model model, ServiceShape service, diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/DirectedRubyCodegenPlugin.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/DirectedRubyCodegenPlugin.java index 38ec26b7a..06d31c0d7 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/DirectedRubyCodegenPlugin.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/DirectedRubyCodegenPlugin.java @@ -17,6 +17,7 @@ import software.amazon.smithy.build.PluginContext; import software.amazon.smithy.build.SmithyBuildPlugin; +import software.amazon.smithy.codegen.core.ShapeGenerationOrder; import software.amazon.smithy.codegen.core.directed.CodegenDirector; import software.amazon.smithy.utils.SmithyInternalApi; @@ -53,6 +54,8 @@ public void execute(PluginContext context) { runner.createDedicatedInputsAndOutputs(); + runner.shapeGenerationOrder(ShapeGenerationOrder.ALPHABETICAL); + runner.run(); } } diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/Hearth.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/Hearth.java index 3c76544ea..8ac4cc3ed 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/Hearth.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/Hearth.java @@ -62,19 +62,14 @@ public final class Hearth { .name("Stubs") .build(); - public static final Symbol RETRY_QUOTA = Symbol.builder() - .namespace("Hearth::Retry", "::") - .name("RetryQuota") - .build(); - - public static final Symbol CLIENT_RATE_LIMITER = Symbol.builder() - .namespace("Hearth::Retry", "::") - .name("ClientRateLimiter") + public static final Symbol HTTP_API_ERROR = Symbol.builder() + .namespace("Hearth::HTTP", "::") + .name("ApiError") .build(); - public static final Symbol API_ERROR = Symbol.builder() + public static final Symbol HTTP_ERROR_INSPECTOR = Symbol.builder() .namespace("Hearth::HTTP", "::") - .name("ApiError") + .name("ErrorInspector") .build(); public static final Symbol XML = Symbol.builder() diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/RubyCodeWriter.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/RubyCodeWriter.java index 2c3495f7a..81ba9c66d 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/RubyCodeWriter.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/RubyCodeWriter.java @@ -15,6 +15,8 @@ package software.amazon.smithy.ruby.codegen; +import java.util.HashSet; +import java.util.Set; import java.util.Stack; import java.util.function.BiFunction; import java.util.function.Consumer; @@ -48,6 +50,7 @@ public class RubyCodeWriter extends SymbolWriter modules = new Stack<>(); + private Set modulesSet = new HashSet<>(); /** * @param namespace namespace to write in @@ -63,6 +66,10 @@ public RubyCodeWriter(String namespace) { } public RubyCodeWriter addModule(String name) { + if (modulesSet.contains(name)) { + return this; + } + modulesSet.add(name); modules.push(name); this.openBlock("module $L", name); return this; @@ -73,7 +80,8 @@ public RubyCodeWriter closeModule() { throw new RuntimeException("No modules were opened"); } - modules.pop(); + String module = modules.pop(); + modulesSet.remove(module); this.closeBlock("end"); return this; } @@ -84,6 +92,11 @@ public void closeAllModules() { } } + public RubyCodeWriter apiPrivate() { + this.write("# @api private"); + return this; + } + /** * Preamble comments will be included in the generated code. * This should be called for writers that are used to generate full files. diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/RubyDependency.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/RubyDependency.java index b605f1e8f..47a34c95f 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/RubyDependency.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/RubyDependency.java @@ -168,6 +168,15 @@ public List getDependencies() { return new ArrayList<>(symbolDependencySet); } + /** + * Get all the Ruby dependencies. + * + * @return list of RubyDependency + */ + public Set getRubyDependencies() { + return dependencies; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/BuilderGeneratorBase.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/BuilderGeneratorBase.java index 5ee93994a..92aa425de 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/BuilderGeneratorBase.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/BuilderGeneratorBase.java @@ -222,6 +222,7 @@ public void render(FileManifest fileManifest) { .includePreamble() .includeRequires() .openBlock("module $L", settings.getModule()) + .apiPrivate() .openBlock("module Builders") .call(() -> renderBuilders()) .closeBlock("end") diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ClientGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ClientGenerator.java index bc0c63f86..93aea74bc 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ClientGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ClientGenerator.java @@ -18,13 +18,9 @@ import java.util.Comparator; import java.util.List; import java.util.Set; -import java.util.TreeSet; import java.util.logging.Logger; -import software.amazon.smithy.build.FileManifest; import software.amazon.smithy.codegen.core.Symbol; -import software.amazon.smithy.codegen.core.SymbolProvider; -import software.amazon.smithy.model.Model; -import software.amazon.smithy.model.knowledge.TopDownIndex; +import software.amazon.smithy.codegen.core.directed.GenerateServiceDirective; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; @@ -44,61 +40,59 @@ * Generate the service Client. */ @SmithyInternalApi -public class ClientGenerator { +public class ClientGenerator extends RubyGeneratorBase { private static final Logger LOGGER = Logger.getLogger(ClientGenerator.class.getName()); - private final GenerationContext context; - private final RubySettings settings; - private final SymbolProvider symbolProvider; - private final Model model; - private final RubyCodeWriter writer; - private final RubyCodeWriter rbsWriter; + private final Set operations; + + private final MiddlewareBuilder middlewareBuilder; + private boolean hasStreamingOperation; - /** - * @param context generation context - */ - public ClientGenerator(GenerationContext context) { - this.context = context; - this.settings = context.settings(); - this.model = context.model(); - this.writer = new RubyCodeWriter(context.settings().getModule()); - this.rbsWriter = new RubyCodeWriter(context.settings().getModule()); - this.symbolProvider = context.symbolProvider(); + public ClientGenerator( + GenerateServiceDirective directive, + MiddlewareBuilder middlewareBuilder + ) { + super(directive); this.hasStreamingOperation = false; + this.operations = directive.operations(); + this.middlewareBuilder = middlewareBuilder; + } + + @Override + String getModule() { + return "Client"; } /** * Render/Generate the service client. - * - * @param middlewareBuilder set of middleware to be added to the client */ - public void render(MiddlewareBuilder middlewareBuilder) { - FileManifest fileManifest = context.fileManifest(); + public void render() { + List additionalFiles = middlewareBuilder.writeAdditionalFiles(context); - writer.includePreamble().includeRequires(); + write(writer -> { - List additionalFiles = - middlewareBuilder.writeAdditionalFiles(context); - for (String require : additionalFiles) { - writer.write("require_relative '$L'", removeRbExtension(require)); - LOGGER.finer("Adding client require: " + require); - } + writer.includePreamble().includeRequires(); - if (additionalFiles.size() > 0) { - writer.write(""); - } + for (String require : additionalFiles) { + writer.write("require_relative '$L'", removeRbExtension(require)); + LOGGER.finer("Adding client require: " + require); + } - writer + if (additionalFiles.size() > 0) { + writer.write(""); + } + + writer .openBlock("module $L", settings.getModule()) .write("# An API client for $L", settings.getService().getName()) .write("# See {#initialize} for a full list of supported configuration options"); - String documentation = new ShapeDocumentationGenerator(model, symbolProvider, context.service()).render(); + String documentation = new ShapeDocumentationGenerator(model, symbolProvider, context.service()).render(); - writer + writer .writeInline("$L", documentation) .openBlock("class Client") .write("include $T", Hearth.CLIENT_STUBS) @@ -106,48 +100,41 @@ public void render(MiddlewareBuilder middlewareBuilder) { .openBlock("\ndef self.middleware") .write("@middleware") .closeBlock("end\n") - .call(() -> renderInitializeMethod()) - .call(() -> renderOperations(middlewareBuilder)) + .call(() -> renderInitializeMethod(writer)) + .call(() -> renderOperations(writer)) .write("\nprivate") - .call(() -> renderApplyMiddlewareMethod()) + .call(() -> renderApplyMiddlewareMethod(writer)) .call(() -> { if (hasStreamingOperation) { - renderOutputStreamMethod(); + renderOutputStreamMethod(writer); } }) .closeBlock("end") .closeBlock("end"); + }); - String fileName = - settings.getGemName() + "/lib/" + settings.getGemName() - + "/client.rb"; - fileManifest.writeFile(fileName, writer.toString()); - LOGGER.fine("Wrote client to " + fileName); + LOGGER.fine("Wrote client to " + rbFile()); } /** * Render/generate the RBS types for the client. */ public void renderRbs() { - FileManifest fileManifest = context.fileManifest(); - - rbsWriter + writeRbs(writer -> { + writer .includePreamble() .openBlock("module $L", settings.getModule()) .openBlock("class Client") .write("include $T\n", Hearth.CLIENT_STUBS) .write("def self.middleware: () -> untyped\n") .write("def initialize: (?untyped config, ?::Hash[untyped, untyped] options) -> void") - .call(() -> renderRbsOperations()) + .call(() -> renderRbsOperations(writer)) .write("") .closeBlock("end") .closeBlock("end"); + }); - String fileName = - settings.getGemName() + "/sig/" + settings.getGemName() - + "/client.rbs"; - fileManifest.writeFile(fileName, rbsWriter.toString()); - LOGGER.fine("Wrote client rbs to " + fileName); + LOGGER.fine("Wrote client rbs to " + rbsFile()); } private Object removeRbExtension(String s) { @@ -157,42 +144,30 @@ private Object removeRbExtension(String s) { return s; } - private void renderInitializeMethod() { + private void renderInitializeMethod(RubyCodeWriter writer) { writer .writeYardParam("Config", "config", "An instance of {Config}") .openBlock("def initialize(config = $L::Config.new, options = {})", settings.getModule()) .write("@config = config") .write("@middleware = $T.new(options[:middleware])", Hearth.MIDDLEWARE_BUILDER) .write("@stubs = $T.new", Hearth.STUBS) - .write("@retry_quota = $T.new", Hearth.RETRY_QUOTA) - .write("@client_rate_limiter = $T.new", Hearth.CLIENT_RATE_LIMITER) .closeBlock("end"); } - private void renderOperations(MiddlewareBuilder middlewareBuilder) { - // Generate each operation for the service. We do this here instead of via the operation visitor method to - // limit it to the operations bound to the service. - TopDownIndex topDownIndex = TopDownIndex.of(model); - Set containedOperations = new TreeSet<>( - topDownIndex.getContainedOperations(context.service())); - containedOperations.stream() - .filter((o) -> !Streaming.isEventStreaming(model, o)) - .sorted(Comparator.comparing((o) -> o.getId().getName())) - .forEach(o -> renderOperation(o, middlewareBuilder)); + private void renderOperations(RubyCodeWriter writer) { + operations.stream() + .filter((o) -> !Streaming.isEventStreaming(model, o)) + .sorted(Comparator.comparing((o) -> o.getId().getName())) + .forEach(o -> renderOperation(writer, o)); } - private void renderRbsOperations() { - // Generate each operation for the service. We do this here instead of via the operation visitor method to - // limit it to the operations bound to the service. - TopDownIndex topDownIndex = TopDownIndex.of(model); - Set containedOperations = new TreeSet<>( - topDownIndex.getContainedOperations(context.service())); - containedOperations.stream() - .sorted(Comparator.comparing((o) -> o.getId().getName())) - .forEach(o -> renderRbsOperation(o)); + private void renderRbsOperations(RubyCodeWriter writer) { + operations.stream() + .sorted(Comparator.comparing((o) -> o.getId().getName())) + .forEach(o -> renderRbsOperation(writer, o)); } - private void renderOperation(OperationShape operation, MiddlewareBuilder middlewareBuilder) { + private void renderOperation(RubyCodeWriter writer, OperationShape operation) { Symbol symbol = symbolProvider.toSymbol(operation); ShapeId inputShapeId = operation.getInputShape(); Shape inputShape = model.expectShape(inputShapeId); @@ -240,16 +215,16 @@ private void renderOperation(OperationShape operation, MiddlewareBuilder middlew LOGGER.finer("Generated client operation method " + operationName); } - private void renderRbsOperation(OperationShape operation) { + private void renderRbsOperation(RubyCodeWriter writer, OperationShape operation) { Symbol symbol = symbolProvider.toSymbol(operation); String operationName = RubyFormatter.toSnakeCase(symbol.getName()); - rbsWriter.write("def $L: (?::Hash[untyped, untyped] params, ?::Hash[untyped, untyped] options)" + writer.write("def $L: (?::Hash[untyped, untyped] params, ?::Hash[untyped, untyped] options)" + "{ () -> untyped } -> untyped", operationName); } - private void renderApplyMiddlewareMethod() { + private void renderApplyMiddlewareMethod(RubyCodeWriter writer) { writer .openBlock( "\ndef apply_middleware(middleware_stack, middleware)") @@ -259,7 +234,7 @@ private void renderApplyMiddlewareMethod() { .closeBlock("end"); } - private void renderOutputStreamMethod() { + private void renderOutputStreamMethod(RubyCodeWriter writer) { writer .openBlock("\ndef output_stream(options = {}, &block)") .write("return options[:output_stream] if options[:output_stream]") diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ConfigGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ConfigGenerator.java index 5eb64ad2c..c02ea9508 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ConfigGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ConfigGenerator.java @@ -18,9 +18,7 @@ import java.util.List; import java.util.logging.Logger; import java.util.stream.Collectors; -import software.amazon.smithy.build.FileManifest; -import software.amazon.smithy.codegen.core.SymbolProvider; -import software.amazon.smithy.model.Model; +import software.amazon.smithy.codegen.core.directed.ContextualDirective; import software.amazon.smithy.ruby.codegen.GenerationContext; import software.amazon.smithy.ruby.codegen.Hearth; import software.amazon.smithy.ruby.codegen.RubyCodeWriter; @@ -34,51 +32,40 @@ * Generate Config class for a Client. */ @SmithyInternalApi -public class ConfigGenerator { +public class ConfigGenerator extends RubyGeneratorBase { private static final Logger LOGGER = Logger.getLogger(ConfigGenerator.class.getName()); - private final GenerationContext context; - private final RubySettings settings; - private final Model model; - private final RubyCodeWriter writer; - private final RubyCodeWriter rbsWriter; - private final SymbolProvider symbolProvider; + private final List clientConfigList; - /** - * @param context generation context - */ - public ConfigGenerator(GenerationContext context) { - this.context = context; - this.settings = context.settings(); - this.model = context.model(); - this.writer = new RubyCodeWriter(context.settings().getModule() + "::Config"); - this.rbsWriter = new RubyCodeWriter(context.settings().getModule() + "::Config"); - this.symbolProvider = context.symbolProvider(); + public ConfigGenerator( + ContextualDirective directive, List clientConfigList) { + super(directive); + this.clientConfigList = clientConfigList; } - /** - * Render/Generate the Config for the client. - * @param clientConfigList list of config to apply to the client. - */ - public void render(List clientConfigList) { - FileManifest fileManifest = context.fileManifest(); + @Override + String getModule() { + return "Config"; + } - String membersBlock = "nil"; - if (!clientConfigList.isEmpty()) { - membersBlock = clientConfigList + public void render() { + write(writer -> { + String membersBlock = "nil"; + if (!clientConfigList.isEmpty()) { + membersBlock = clientConfigList .stream() .map(clientConfig -> RubyFormatter.asSymbol( RubySymbolProvider.toMemberName(clientConfig.getName()))) .collect(Collectors.joining(",\n")); - } - membersBlock += ","; + } + membersBlock += ","; - writer + writer .includePreamble() .includeRequires() .openBlock("module $L", settings.getModule()) - .call(() -> renderConfigDocumentation(clientConfigList)) + .call(() -> renderConfigDocumentation(writer)) .openBlock("Config = ::Struct.new(") .write(membersBlock) .write("keyword_init: true") @@ -86,38 +73,30 @@ public void render(List clientConfigList) { .indent() .write("include $T", Hearth.CONFIGURATION) .write("\nprivate\n") - .call(() -> renderValidateMethod(clientConfigList)) + .call(() -> renderValidateMethod(writer)) .write("") - .call(() -> renderDefaultsMethod(clientConfigList)) + .call(() -> renderDefaultsMethod(writer)) .closeBlock("end") .closeBlock("end\n"); + }); - String fileName = - settings.getGemName() + "/lib/" + settings.getGemName() - + "/config.rb"; - fileManifest.writeFile(fileName, writer.toString()); - LOGGER.fine("Wrote config to " + fileName); + LOGGER.fine("Wrote config to " + rbFile()); } /** * Render/generate the RBS types for Config. */ public void renderRbs() { - FileManifest fileManifest = context.fileManifest(); - - rbsWriter + writeRbs(writer -> { + writer .openBlock("module $L", settings.getModule()) .write("Config: untyped") .closeBlock("end"); - - String fileName = - settings.getGemName() + "/sig/" + settings.getGemName() - + "/config.rbs"; - fileManifest.writeFile(fileName, rbsWriter.toString()); - LOGGER.fine("Wrote config rbs to " + fileName); + }); + LOGGER.fine("Wrote config rbs to " + rbsFile()); } - private void renderConfigDocumentation(List clientConfigList) { + private void renderConfigDocumentation(RubyCodeWriter writer) { writer.writeYardMethod("initialize(*options)", () -> { clientConfigList.forEach((clientConfig) -> { String member = RubyFormatter.asSymbol(RubySymbolProvider.toMemberName(clientConfig.getName())); @@ -135,7 +114,7 @@ private void renderConfigDocumentation(List clientConfigList) { }); } - private void renderValidateMethod(List clientConfigList) { + private void renderValidateMethod(RubyCodeWriter writer) { writer.openBlock("def validate!"); clientConfigList.stream().forEach(clientConfig -> { String member = RubySymbolProvider.toMemberName(clientConfig.getName()); @@ -150,7 +129,7 @@ private void renderValidateMethod(List clientConfigList) { writer.closeBlock("end"); } - private void renderDefaultsMethod(List clientConfigList) { + private void renderDefaultsMethod(RubyCodeWriter writer) { writer .openBlock("def self.defaults") .openBlock("@defaults ||= {"); diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/EnumGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/EnumGenerator.java new file mode 100644 index 000000000..a4f346abd --- /dev/null +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/EnumGenerator.java @@ -0,0 +1,104 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.ruby.codegen.generators; + +import java.util.List; +import java.util.stream.Collectors; +import software.amazon.smithy.codegen.core.directed.GenerateEnumDirective; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.traits.EnumDefinition; +import software.amazon.smithy.model.traits.EnumTrait; +import software.amazon.smithy.ruby.codegen.GenerationContext; +import software.amazon.smithy.ruby.codegen.RubySettings; +import software.amazon.smithy.utils.SmithyInternalApi; + +@SmithyInternalApi +public final class EnumGenerator extends RubyGeneratorBase { + + private final Shape shape; + + public EnumGenerator(GenerateEnumDirective directive) { + super(directive); + this.shape = directive.shape(); + } + + @Override + String getModule() { + return "Types"; + } + + public void render() { + write(writer -> { + final EnumTrait enumTrait = shape.expectTrait(EnumTrait.class); + + List enumDefinitions = enumTrait.getValues().stream() + .filter(value -> value.getName().isPresent()) + .collect(Collectors.toList()); + + // only write out a module if there is at least one enum constant + if (enumDefinitions.size() > 0) { + String shapeName = symbolProvider.toSymbol(shape).getName(); + + writer + .writeDocstring("Includes enum constants for " + shapeName) + .openBlock("module $L", shapeName); + + enumDefinitions.forEach(enumDefinition -> { + String enumName = enumDefinition.getName().get(); + String enumValue = enumDefinition.getValue(); + String enumDocumentation = enumDefinition.getDocumentation() + .orElse("No documentation available."); + writer.writeDocstring(enumDocumentation); + if (enumDefinition.isDeprecated()) { + writer.writeYardDeprecated("This enum value is deprecated.", ""); + } + if (!enumDefinition.getTags().isEmpty()) { + String enumTags = enumDefinition.getTags().stream() + .map((tag) -> "\"" + tag + "\"") + .collect(Collectors.joining(", ")); + writer.writeDocstring("Tags: [" + enumTags + "]"); + } + writer.write("$L = $S\n", enumName, enumValue); + }); + + writer + .unwrite("\n") + .closeBlock("end\n"); + } + }); + + writeRbs(writer -> { + // Only write out string shapes for enums + EnumTrait enumTrait = shape.expectTrait(EnumTrait.class); + List enumDefinitions = enumTrait.getValues().stream() + .filter(value -> value.getName().isPresent()) + .collect(Collectors.toList()); + + // only write out a module if there is at least one enum constant + if (enumDefinitions.size() > 0) { + String shapeName = symbolProvider.toSymbol(shape).getName(); + writer.openBlock("module $L", shapeName); + enumDefinitions.forEach(enumDefinition -> { + String enumName = enumDefinition.getName().get(); + writer.write("$L: ::String\n", enumName); + }); + writer + .unwrite("\n") + .closeBlock("end\n"); + } + }); + } +} diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ErrorsGeneratorBase.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ErrorsGeneratorBase.java index 91881e93b..5bdaa3477 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ErrorsGeneratorBase.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ErrorsGeneratorBase.java @@ -157,7 +157,7 @@ public void renderRbs(FileManifest fileManifest) { private void renderBaseErrors() { writer .write("\n# Base class for all errors returned by this service") - .write("class ApiError < $T; end", Hearth.API_ERROR) + .write("class ApiError < $T; end", Hearth.HTTP_API_ERROR) .write("\n# Base class for all errors returned where the client is at fault.") .write("# These are generally errors with 4XX HTTP status codes.") .write("class ApiClientError < ApiError; end") @@ -179,7 +179,7 @@ private void renderBaseErrors() { private void renderRbsBaseErrors() { rbsWriter - .write("\nclass ApiError < $T", Hearth.API_ERROR) + .write("\nclass ApiError < $T", Hearth.HTTP_API_ERROR) .write("def initialize: (request_id: untyped request_id, **untyped kwargs) -> void\n") .write("attr_reader request_id: untyped") .write("end") diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/GemspecGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/GemspecGenerator.java index f41638ecf..533094471 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/GemspecGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/GemspecGenerator.java @@ -29,6 +29,8 @@ package software.amazon.smithy.ruby.codegen.generators; +import java.util.HashSet; +import java.util.Set; import software.amazon.smithy.build.FileManifest; import software.amazon.smithy.ruby.codegen.GenerationContext; import software.amazon.smithy.ruby.codegen.RubyCodeWriter; @@ -69,8 +71,15 @@ public void render() { .write("spec.files = Dir['lib/**/*.rb']") .write("") .call(() -> { + // determine set of indirect dependencies - covered by requiring another + Set indirectDependencies = new HashSet<>(); + context.getRubyDependencies().forEach(rubyDependency -> { + indirectDependencies.addAll(rubyDependency.getRubyDependencies()); + }); + context.getRubyDependencies().forEach((rubyDependency -> { - if (rubyDependency.getType() != RubyDependency.Type.STANDARD_LIBRARY) { + if (rubyDependency.getType() != RubyDependency.Type.STANDARD_LIBRARY + && !indirectDependencies.contains(rubyDependency)) { writer.write("spec.add_runtime_dependency '$L', '$L'", rubyDependency.getGemName(), rubyDependency.getVersion()); } diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/HttpProtocolTestGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/HttpProtocolTestGenerator.java index bf34dfb1c..6eefd874c 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/HttpProtocolTestGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/HttpProtocolTestGenerator.java @@ -332,13 +332,16 @@ private void renderResponseMiddleware(HttpResponseTestCase testCase) { private void renderResponseMiddlewareBody(Optional body) { if (body.isPresent()) { - writer.write("response.body = StringIO.new('$L')", body.get()); + writer.write("response.body.write('$L')", body.get()); + writer.write("response.body.rewind"); } } private void renderResponseMiddlewareHeaders(Map headers) { - if (!headers.isEmpty()) { - writer.write("response.headers = Hearth::HTTP::Headers.new($L)", getRubyHashFromMap(headers)); + Iterator> iterator = headers.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry header = iterator.next(); + writer.write("response.headers['$L'] = '$L'", header.getKey(), header.getValue()); } } @@ -346,10 +349,9 @@ private void renderRequestMiddleware(HttpRequestTestCase testCase) { writer .openBlock("middleware = Hearth::MiddlewareBuilder.before_send do |input, context|") .write("request = context.request") - .write("request_uri = URI.parse(request.url)") .write("expect(request.http_method).to eq('$L')", testCase.getMethod()) .call(() -> renderRequestMiddlewareHost(testCase.getResolvedHost())) - .write("expect(request_uri.path).to eq('$L')", testCase.getUri()) + .write("expect(request.uri.path).to eq('$L')", testCase.getUri()) .call(() -> renderRequestMiddlewareQueryParams(testCase.getQueryParams())) .call(() -> renderRequestMiddlewareForbidQueryParams(testCase.getForbidQueryParams())) .call(() -> renderRequestMiddlewareRequireQueryParams(testCase.getRequireQueryParams())) @@ -363,7 +365,7 @@ private void renderRequestMiddleware(HttpRequestTestCase testCase) { private void renderRequestMiddlewareHost(Optional resolvedHost) { if (resolvedHost.isPresent()) { - writer.write("expect(request_uri.host).to eq('$L')", resolvedHost.get()); + writer.write("expect(request.uri.host).to eq('$L')", resolvedHost.get()); } } @@ -378,7 +380,7 @@ private void renderRequestMiddlewareBody(Optional body, Optional if (body.get().length() > 0) { writer .write("expect($T.parse(request.body.read)).to " - + "match_xml_node(Hearth::XML.parse('$L'))", + + "match_xml_node($T.parse('$L'))", Hearth.XML, body.get()) .addUseImports(RubyDependency.HEARTH_XML_MATCHER); } else { @@ -427,7 +429,7 @@ private void renderRequestMiddlewareQueryParams(List queryParams) { writer .write("expected_query = $T.parse($L.join('&'))", RubyImportContainer.CGI, getRubyArrayFromList(queryParams)) - .write("actual_query = $T.parse(request_uri.query)", + .write("actual_query = $T.parse(request.uri.query)", RubyImportContainer.CGI) .openBlock("expected_query.each do |k, v|") .write("expect(actual_query[k]).to eq(v)") @@ -439,7 +441,7 @@ private void renderRequestMiddlewareForbidQueryParams(List forbidQueryPa if (!forbidQueryParams.isEmpty()) { writer .write("forbid_query = $L", getRubyArrayFromList(forbidQueryParams)) - .write("actual_query = $T.parse(request_uri.query)", + .write("actual_query = $T.parse(request.uri.query)", RubyImportContainer.CGI) .openBlock("forbid_query.each do |query|") .write("expect(actual_query.key?(query)).to be false") @@ -451,7 +453,7 @@ private void renderRequestMiddlewareRequireQueryParams(List requireQuery if (!requireQueryParams.isEmpty()) { writer .write("require_query = $L", getRubyArrayFromList(requireQueryParams)) - .write("actual_query = $T.parse(request_uri.query)", + .write("actual_query = $T.parse(request.uri.query)", RubyImportContainer.CGI) .openBlock("require_query.each do |query|") .write("expect(actual_query.key?(query)).to be true") diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/IntEnumGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/IntEnumGenerator.java new file mode 100644 index 000000000..b989605ea --- /dev/null +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/IntEnumGenerator.java @@ -0,0 +1,55 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.ruby.codegen.generators; + +import software.amazon.smithy.codegen.core.directed.GenerateIntEnumDirective; +import software.amazon.smithy.model.shapes.IntEnumShape; +import software.amazon.smithy.ruby.codegen.GenerationContext; +import software.amazon.smithy.ruby.codegen.RubySettings; +import software.amazon.smithy.utils.SmithyInternalApi; + +@SmithyInternalApi +public final class IntEnumGenerator extends RubyGeneratorBase { + + private final IntEnumShape shape; + + public IntEnumGenerator(GenerateIntEnumDirective directive) { + super(directive); + this.shape = (IntEnumShape) directive.shape(); + } + + @Override + String getModule() { + return "Types"; + } + + public void render() { + write(writer -> { + // only write out a module if there is at least one enum constant + if (shape.getEnumValues().size() > 0) { + String shapeName = symbolProvider.toSymbol(shape).getName(); + + writer.writeDocstring("Includes enum constants for " + shapeName) + .addModule(shapeName); + + shape.getEnumValues() + .forEach((enumName, enumValue) -> writer.write("$L = $L\n", enumName, enumValue)); + + writer.unwrite("\n").closeModule(); + } + }); + } +} diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ModuleGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ModuleGenerator.java index a3d57c045..088e380c9 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ModuleGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ModuleGenerator.java @@ -15,11 +15,14 @@ package software.amazon.smithy.ruby.codegen.generators; +import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.logging.Logger; -import software.amazon.smithy.build.FileManifest; +import software.amazon.smithy.codegen.core.directed.ContextualDirective; import software.amazon.smithy.ruby.codegen.GenerationContext; -import software.amazon.smithy.ruby.codegen.RubyCodeWriter; +import software.amazon.smithy.ruby.codegen.RubyDependency; import software.amazon.smithy.ruby.codegen.RubySettings; import software.amazon.smithy.utils.SmithyInternalApi; @@ -34,42 +37,52 @@ public class ModuleGenerator { }; private final GenerationContext context; + private final RubySettings settings; - public ModuleGenerator(GenerationContext context) { - this.context = context; + public ModuleGenerator(ContextualDirective directive) { + this.context = directive.context(); + this.settings = directive.settings(); } - public void render(List additionalFiles) { - FileManifest fileManifest = context.fileManifest(); - RubySettings settings = context.settings(); - RubyCodeWriter writer = new RubyCodeWriter(context.settings().getModule()); + public void render() { + List additionalFiles = context.integrations().stream() + .map((integration) -> integration.writeAdditionalFiles(context)) + .flatMap(Collection::stream) + .toList(); - writer.includePreamble().includeRequires(); - context.getRubyDependencies().forEach((rubyDependency -> { - writer.write("require '$L'", rubyDependency.getImportPath()); - })); - writer.write("\n"); + String fileName = + settings.getGemName() + "/lib/" + settings.getGemName() + ".rb"; - for (String require : DEFAULT_REQUIRES) { - writer.write("require_relative '$L/$L'", settings.getGemName(), - require); - } + context.writerDelegator().useFileWriter(fileName, settings.getModule(), writer -> { + writer.includePreamble().includeRequires(); + // determine set of indirect dependencies - covered by requiring another + Set indirectDependencies = new HashSet<>(); + context.getRubyDependencies().forEach(rubyDependency -> { + indirectDependencies.addAll(rubyDependency.getRubyDependencies()); + }); - for (String require : additionalFiles) { - writer.write("require_relative '$L'", require); - LOGGER.finer("Adding additional module require: " + require); - } + context.getRubyDependencies().forEach((rubyDependency -> { + if (!indirectDependencies.contains(rubyDependency)) { + writer.write("require '$L'", rubyDependency.getImportPath()); + } + })); + writer.write("\n"); - writer.write(""); + for (String require : DEFAULT_REQUIRES) { + writer.write("require_relative '$L/$L'", settings.getGemName(), require); + } - writer.openBlock("module $L", settings.getModule()) - .write("GEM_VERSION = '$L'", settings.getGemVersion()) - .closeBlock("end"); + for (String require : additionalFiles) { + writer.write("require_relative '$L'", require); + LOGGER.finer("Adding additional module require: " + require); + } - String fileName = - settings.getGemName() + "/lib/" + settings.getGemName() + ".rb"; + writer.write(""); - fileManifest.writeFile(fileName, writer.toString()); + writer.openBlock("module $L", settings.getModule()) + .write("GEM_VERSION = '$L'", settings.getGemVersion()) + .closeBlock("end"); + }); LOGGER.fine("Wrote module file to " + fileName); } } diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/PaginatorsGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/PaginatorsGenerator.java index d56c1877b..c2755fca6 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/PaginatorsGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/PaginatorsGenerator.java @@ -18,9 +18,7 @@ import java.util.Optional; import java.util.logging.Logger; import java.util.stream.Collectors; -import software.amazon.smithy.build.FileManifest; -import software.amazon.smithy.codegen.core.SymbolProvider; -import software.amazon.smithy.model.Model; +import software.amazon.smithy.codegen.core.directed.ContextualDirective; import software.amazon.smithy.model.knowledge.PaginatedIndex; import software.amazon.smithy.model.knowledge.PaginationInfo; import software.amazon.smithy.model.knowledge.TopDownIndex; @@ -31,65 +29,51 @@ import software.amazon.smithy.utils.SmithyInternalApi; @SmithyInternalApi -public class PaginatorsGenerator { +public class PaginatorsGenerator extends RubyGeneratorBase { private static final Logger LOGGER = Logger.getLogger(PaginatorsGenerator.class.getName()); - private final GenerationContext context; - private final RubySettings settings; - private final Model model; - private final RubyCodeWriter writer; - private final RubyCodeWriter rbsWriter; - private final SymbolProvider symbolProvider; - - public PaginatorsGenerator(GenerationContext context) { - this.context = context; - this.settings = context.settings(); - this.model = context.model(); - this.writer = new RubyCodeWriter(context.settings().getModule() + "::Paginators"); - this.rbsWriter = new RubyCodeWriter(context.settings().getModule() + "::Paginators"); - this.symbolProvider = context.symbolProvider(); + public PaginatorsGenerator(ContextualDirective directive) { + super(directive); } - public void render() { - FileManifest fileManifest = context.fileManifest(); + @Override + String getModule() { + return "Paginators"; + } - writer + public void render() { + write(writer -> { + writer .includePreamble() .includeRequires() .openBlock("module $L", settings.getModule()) .openBlock("module Paginators") - .call(() -> renderPaginators()) + .call(() -> renderPaginators(writer)) .write("") .closeBlock("end") .closeBlock("end"); - String fileName = settings.getGemName() + "/lib/" + settings.getGemName() + "/paginators.rb"; - fileManifest.writeFile(fileName, writer.toString()); - LOGGER.fine("Wrote paginators to " + fileName); + }); + LOGGER.fine("Wrote paginators to " + rbFile()); } public void renderRbs() { - FileManifest fileManifest = context.fileManifest(); - - rbsWriter + writeRbs(writer -> { + writer .includePreamble() .openBlock("module $L", settings.getModule()) .openBlock("module Paginators") - .call(() -> renderRbsPaginators()) + .call(() -> renderRbsPaginators(writer)) .write("") .closeBlock("end") .closeBlock("end"); - - String typesFile = - settings.getGemName() + "/sig/" + settings.getGemName() - + "/paginators.rbs"; - fileManifest.writeFile(typesFile, rbsWriter.toString()); - LOGGER.fine("Wrote paginators types to " + typesFile); + }); + LOGGER.fine("Wrote paginators types to " + rbsFile()); } - private void renderPaginators() { + private void renderPaginators(RubyCodeWriter writer) { TopDownIndex topDownIndex = TopDownIndex.of(model); PaginatedIndex paginatedIndex = PaginatedIndex.of(model); @@ -99,12 +83,12 @@ private void renderPaginators() { if (paginationInfoOptional.isPresent()) { PaginationInfo paginationInfo = paginationInfoOptional.get(); String operationName = symbolProvider.toSymbol(operation).getName(); - renderPaginator(operationName, paginationInfo); + renderPaginator(writer, operationName, paginationInfo); } }); } - private void renderRbsPaginators() { + private void renderRbsPaginators(RubyCodeWriter writer) { TopDownIndex topDownIndex = TopDownIndex.of(model); PaginatedIndex paginatedIndex = PaginatedIndex.of(model); @@ -114,33 +98,33 @@ private void renderRbsPaginators() { if (paginationInfoOptional.isPresent()) { PaginationInfo paginationInfo = paginationInfoOptional.get(); String operationName = symbolProvider.toSymbol(operation).getName(); - renderRbsPaginator(operationName, paginationInfo); + renderRbsPaginator(writer, operationName, paginationInfo); } }); } - private void renderPaginator(String operationName, PaginationInfo paginationInfo) { + private void renderPaginator(RubyCodeWriter writer, String operationName, PaginationInfo paginationInfo) { writer .write("") .openBlock("class $L", operationName) - .call(() -> renderPaginatorInitializeDocumentation(operationName)) + .call(() -> renderPaginatorInitializeDocumentation(writer, operationName)) .openBlock("def initialize(client, params = {}, options = {})") .write("@params = params") .write("@options = options") .write("@client = client") .closeBlock("end") - .call(() -> renderPaginatorPages(operationName, paginationInfo)) + .call(() -> renderPaginatorPages(writer, operationName, paginationInfo)) .call(() -> { if (!paginationInfo.getItemsMemberPath().isEmpty()) { - renderPaginatorItems(paginationInfo, operationName); + renderPaginatorItems(writer, paginationInfo, operationName); } }) .closeBlock("end"); LOGGER.finer("Generated paginator for " + operationName); } - private void renderRbsPaginator(String operationName, PaginationInfo paginationInfo) { - rbsWriter + private void renderRbsPaginator(RubyCodeWriter writer, String operationName, PaginationInfo paginationInfo) { + writer .write("") .openBlock("class $L", operationName) .write("def initialize: (untyped client, ?::Hash[untyped, untyped] params, " @@ -148,13 +132,13 @@ private void renderRbsPaginator(String operationName, PaginationInfo paginationI .write("def pages: () -> untyped") .call(() -> { if (!paginationInfo.getItemsMemberPath().isEmpty()) { - rbsWriter.write("def items: () -> untyped"); + writer.write("def items: () -> untyped"); } }) .closeBlock("end"); } - private void renderPaginatorInitializeDocumentation(String operationName) { + private void renderPaginatorInitializeDocumentation(RubyCodeWriter writer, String operationName) { String snakeOperationName = RubyFormatter.toSnakeCase(operationName); writer.writeDocs((w) -> w @@ -163,7 +147,7 @@ private void renderPaginatorInitializeDocumentation(String operationName) { .write("@param [Hash] options (see Client#$L)", snakeOperationName)); } - private void renderPaginatorPages(String operationName, PaginationInfo paginationInfo) { + private void renderPaginatorPages(RubyCodeWriter writer, String operationName, PaginationInfo paginationInfo) { String inputToken = symbolProvider.toMemberName(paginationInfo.getInputTokenMember()); String outputToken = paginationInfo.getOutputTokenMemberPath().stream() .map((member) -> symbolProvider.toMemberName(member)) @@ -171,7 +155,7 @@ private void renderPaginatorPages(String operationName, PaginationInfo paginatio String snakeOperationName = RubyFormatter.toSnakeCase(operationName); writer - .call(() -> renderPaginatorPagesDocumentation(snakeOperationName)) + .call(() -> renderPaginatorPagesDocumentation(writer, snakeOperationName)) .openBlock("def pages") .write("params = @params") .openBlock("Enumerator.new do |e|") @@ -190,20 +174,20 @@ private void renderPaginatorPages(String operationName, PaginationInfo paginatio .closeBlock("end"); } - private void renderPaginatorPagesDocumentation(String snakeOperationName) { + private void renderPaginatorPagesDocumentation(RubyCodeWriter writer, String snakeOperationName) { writer.writeDocs((w) -> w .write("Iterate all response pages of the $L operation.", snakeOperationName) .write("@return [Enumerator]")); } - private void renderPaginatorItems(PaginationInfo paginationInfo, String operationName) { + private void renderPaginatorItems(RubyCodeWriter writer, PaginationInfo paginationInfo, String operationName) { String items = paginationInfo.getItemsMemberPath().stream() .map((member) -> symbolProvider.toMemberName(member)) .collect(Collectors.joining("&.")); writer .write("") - .call(() -> renderPaginatorItemsDocumentation(operationName)) + .call(() -> renderPaginatorItemsDocumentation(writer, operationName)) .openBlock("def items") .openBlock("Enumerator.new do |e|") .openBlock("pages.each do |page|") @@ -215,7 +199,7 @@ private void renderPaginatorItems(PaginationInfo paginationInfo, String operatio .closeBlock("end"); } - private void renderPaginatorItemsDocumentation(String operationName) { + private void renderPaginatorItemsDocumentation(RubyCodeWriter writer, String operationName) { String snakeOperationName = RubyFormatter.toSnakeCase(operationName); writer.writeDocs((w) -> w .write("Iterate all items from pages in the $L operation.", snakeOperationName) diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParamsGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParamsGenerator.java index c5f85d145..57ccb7c88 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParamsGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParamsGenerator.java @@ -20,9 +20,9 @@ import java.util.Optional; import java.util.logging.Logger; import java.util.stream.Collectors; -import software.amazon.smithy.build.FileManifest; import software.amazon.smithy.codegen.core.Symbol; import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.codegen.core.directed.ContextualDirective; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.neighbor.Walker; import software.amazon.smithy.model.node.Node; @@ -63,44 +63,35 @@ import software.amazon.smithy.utils.SmithyInternalApi; @SmithyInternalApi -public class ParamsGenerator extends ShapeVisitor.Default { +public class ParamsGenerator extends RubyGeneratorBase { private static final Logger LOGGER = Logger.getLogger(ParamsGenerator.class.getName()); - private final GenerationContext context; - private final RubySettings settings; - private final Model model; - private final RubyCodeWriter writer; - private final SymbolProvider symbolProvider; - - public ParamsGenerator(GenerationContext context) { - this.context = context; - this.settings = context.settings(); - this.model = context.model(); - this.writer = new RubyCodeWriter(context.settings().getModule() + "::Params"); - this.symbolProvider = context.symbolProvider(); + public ParamsGenerator(ContextualDirective directive) { + super(directive); + } + + @Override + String getModule() { + return "Params"; } public void render() { - FileManifest fileManifest = context.fileManifest(); - - writer - .includePreamble() - .includeRequires() - .addModule(settings.getModule()) - .addModule("Params") - .call(() -> renderParams()) - .write("") - .closeAllModules(); - - String fileName = - settings.getGemName() + "/lib/" + settings.getGemName() - + "/params.rb"; - fileManifest.writeFile(fileName, writer.toString()); - LOGGER.fine("Wrote params to " + fileName); + write(writer -> { + writer + .includePreamble() + .includeRequires() + .addModule(settings.getModule()) + .apiPrivate() + .addModule("Params") + .call(() -> renderParams(writer)) + .write("") + .closeAllModules(); + }); + LOGGER.fine("Wrote params to " + rbFile()); } - private void renderParams() { + private void renderParams(RubyCodeWriter writer) { Model modelWithoutTraitShapes = ModelTransformer.create() .getModelWithoutTraitShapes(model); @@ -108,440 +99,451 @@ private void renderParams() { .walkShapes(context.service()) .stream() .sorted(Comparator.comparing((o) -> o.getId().getName())) - .forEach((shape) -> shape.accept(this)); - } - - @Override - protected Void getDefault(Shape shape) { - return null; - } - - @Override - public Void structureShape(StructureShape structureShape) { - writer - .write("") - .openBlock("module $L", symbolProvider.toSymbol(structureShape).getName()) - .openBlock("def self.build(params, context: '')") - .call(() -> renderBuilderForStructureMembers( - context.symbolProvider().toSymbol(structureShape), structureShape.members())) - .closeBlock("end") - .closeBlock("end"); - return null; - } - - private void renderBuilderForStructureMembers(Symbol symbol, Collection members) { - writer - .write("$T.validate_types!(params, ::Hash, $T, context: context)", - Hearth.VALIDATOR, symbol) - .write("type = $T.new", symbol); - - members.forEach(member -> { - Shape target = model.expectShape(member.getTarget()); - String memberName = symbolProvider.toMemberName(member); - String memberSetter = "type." + memberName + " = "; - String symbolName = RubyFormatter.asSymbol(memberName); - String input = "params[" + symbolName + "]"; - String contextKey = "\"#{context}[" + symbolName + "]\""; - target.accept(new MemberBuilder(model, writer, context.symbolProvider(), - memberSetter, input, contextKey, member, true)); - }); - - writer.write("type"); - } - - @Override - public Void listShape(ListShape listShape) { - Shape memberTarget = - model.expectShape(listShape.getMember().getTarget()); - - writer - .write("") - .openBlock("module $L", symbolProvider.toSymbol(listShape).getName()) - .openBlock("def self.build(params, context: '')") - .write("$T.validate_types!(params, ::Array, context: context)", Hearth.VALIDATOR) - .write("data = []") - .call(() -> { - if (isComplexShape(memberTarget)) { - writer.openBlock("params.each_with_index do |element, index|"); - } else { - writer.openBlock("params.each do |element|"); - } - }) - .call(() -> memberTarget - .accept(new MemberBuilder(model, writer, symbolProvider, "data << ", - "element", "\"#{context}[#{index}]\"", - listShape.getMember(), - !listShape.hasTrait(SparseTrait.class)))) - .closeBlock("end") - .write("data") - .closeBlock("end") - .closeBlock("end"); - return null; - } - - @Override - public Void mapShape(MapShape mapShape) { - Shape valueTarget = model.expectShape(mapShape.getValue().getTarget()); - - writer - .write("") - .openBlock("module $L", symbolProvider.toSymbol(mapShape).getName()) - .openBlock("def self.build(params, context: '')") - .write("$T.validate_types!(params, ::Hash, context: context)", Hearth.VALIDATOR) - .write("data = {}") - .openBlock("params.each do |key, value|") - .call(() -> valueTarget - .accept(new MemberBuilder(model, writer, context.symbolProvider(), "data[key] = ", - "value", "\"#{context}[:#{key}]\"", mapShape.getValue(), - !mapShape.hasTrait(SparseTrait.class)))) - .closeBlock("end") - .write("data") - .closeBlock("end") - .closeBlock("end"); - return null; - } - - @Override - public Void unionShape(UnionShape shape) { - String name = symbolProvider.toSymbol(shape).getName(); - Symbol typeSymbol = context.symbolProvider().toSymbol(shape); - - writer - .write("") - .openBlock("module $L", name) - .openBlock("def self.build(params, context: '')") - .write("return params if params.is_a?($T)", typeSymbol) - .write("$T.validate_types!(params, ::Hash, $T, context: context)", - Hearth.VALIDATOR, typeSymbol) - .openBlock("unless params.size == 1") - .write("raise ArgumentError,") - .indent(3) - .write("\"Expected #{context} to have exactly one member, got: #{params}\"") - .dedent(3) - .closeBlock("end") - .write("key, value = params.flatten") - .write("case key"); //start a case statement. This does NOT indent - - for (MemberShape member : shape.members()) { - Shape target = model.expectShape(member.getTarget()); - String memberClassName = symbolProvider.toMemberName(member); - String memberName = RubyFormatter.asSymbol(memberClassName); - writer.write("when $L", memberName) - .indent() - .openBlock("$T.new(", context.symbolProvider().toSymbol(member)); - String input = "params[" + memberName + "]"; - String contextString = "\"#{context}[" + memberName + "]\""; - target.accept(new MemberBuilder(model, writer, symbolProvider, "", input, contextString, - member, false)); - writer.closeBlock(")") - .dedent(); - } - String expectedMembers = - shape.members().stream().map((member) -> RubyFormatter.asSymbol(member.getMemberName())) - .collect(Collectors.joining(", ")); - writer.write("else") - .indent() - .write("raise ArgumentError,") - .indent(3) - .write("\"Expected #{context} to have one of $L set\"", expectedMembers) - .dedent(4); - writer.write("end") //end of case statement, NOT indented - .closeBlock("end") - .closeBlock("end"); - return null; + .forEach((shape) -> shape.accept(new Visitor(writer))); } - private boolean isComplexShape(Shape shape) { - return shape.isStructureShape() || shape.isListShape() || shape.isMapShape() - || shape.isUnionShape() || shape.isOperationShape(); - } + private final class Visitor extends ShapeVisitor.Default { - private static class MemberBuilder extends ShapeVisitor.Default { - private final Model model; private final RubyCodeWriter writer; - private final SymbolProvider symbolProvider; - private final String memberSetter; - private final String input; - private final String context; - private final MemberShape memberShape; - private final Optional defaultValue; - private final boolean checkRequired; - private final String rubySymbol; - - MemberBuilder( - Model model, - RubyCodeWriter writer, - SymbolProvider symbolProvider, - String memberSetter, - String input, - String context, - MemberShape memberShape, - boolean checkRequired - ) { - this.model = model; + + private Visitor(RubyCodeWriter writer) { this.writer = writer; - this.symbolProvider = symbolProvider; - this.memberSetter = memberSetter; - this.input = input; - this.context = context; - this.memberShape = memberShape; - this.checkRequired = checkRequired; - this.rubySymbol = RubyFormatter.asSymbol(symbolProvider.toMemberName(memberShape)); - - // Note: No need to check for box trait for V1 Smithy models. - // Smithy convert V1 to V2 model and populate Default trait automatically - boolean containsRequiredAndDefaultTraits = - memberShape.hasTrait(DefaultTrait.class) && memberShape.hasTrait(RequiredTrait.class); - - if (containsRequiredAndDefaultTraits) { - Shape targetShape = model.expectShape(memberShape.getTarget()); - this.defaultValue = Optional.of(targetShape.accept(new DefaultValueRetriever(model, memberShape))); - } else { - this.defaultValue = Optional.empty(); - } } @Override protected Void getDefault(Shape shape) { - if (defaultValue.isPresent()) { - writer.write("$1Lparams.fetch($2L, $3L)", memberSetter, rubySymbol, defaultValue.get()); - } else { - writer.write("$L$L", memberSetter, input); - } return null; } @Override - public Void blobShape(BlobShape shape) { - if (shape.hasTrait(StreamingTrait.class)) { - writer - .write("io = $L || StringIO.new", input) - .openBlock("unless io.respond_to?(:read) " - + "|| io.respond_to?(:readpartial)") - .write("io = StringIO.new(io)") - .closeBlock("end") - .write("$Lio", memberSetter); - } else { - getDefault(shape); - } + public Void structureShape(StructureShape structureShape) { + writer + .write("") + .openBlock("module $L", symbolProvider.toSymbol(structureShape).getName()) + .openBlock("def self.build(params, context: '')") + .call(() -> renderBuilderForStructureMembers( + context.symbolProvider().toSymbol(structureShape), structureShape.members())) + .closeBlock("end") + .closeBlock("end"); return null; } - @Override - public Void stringShape(StringShape shape) { - if (memberShape.hasTrait(IdempotencyTokenTrait.class) - || shape.hasTrait(IdempotencyTokenTrait.class)) { - writer.write("$L$L || $T.uuid", memberSetter, input, RubyImportContainer.SECURE_RANDOM); - } else { - getDefault(shape); - } - return null; - } + private void renderBuilderForStructureMembers(Symbol symbol, Collection members) { + writer + .write("$T.validate_types!(params, ::Hash, $T, context: context)", + Hearth.VALIDATOR, symbol) + .write("type = $T.new", symbol); - @Override - public Void listShape(ListShape shape) { - defaultComplex(shape); - return null; + members.forEach(member -> { + Shape target = model.expectShape(member.getTarget()); + String memberName = symbolProvider.toMemberName(member); + String memberSetter = "type." + memberName + " = "; + String symbolName = RubyFormatter.asSymbol(memberName); + String input = "params[" + symbolName + "]"; + String contextKey = "\"#{context}[" + symbolName + "]\""; + target.accept(new MemberBuilder(model, writer, context.symbolProvider(), + memberSetter, input, contextKey, member, true)); + }); + + writer.write("type"); } @Override - public Void mapShape(MapShape shape) { - defaultComplex(shape); + public Void listShape(ListShape listShape) { + Shape memberTarget = + model.expectShape(listShape.getMember().getTarget()); + + writer + .write("") + .openBlock("module $L", symbolProvider.toSymbol(listShape).getName()) + .openBlock("def self.build(params, context: '')") + .write("$T.validate_types!(params, ::Array, context: context)", Hearth.VALIDATOR) + .write("data = []") + .call(() -> { + if (isComplexShape(memberTarget)) { + writer.openBlock("params.each_with_index do |element, index|"); + } else { + writer.openBlock("params.each do |element|"); + } + }) + .call(() -> memberTarget + .accept(new MemberBuilder(model, writer, symbolProvider, "data << ", + "element", "\"#{context}[#{index}]\"", + listShape.getMember(), + !listShape.hasTrait(SparseTrait.class)))) + .closeBlock("end") + .write("data") + .closeBlock("end") + .closeBlock("end"); return null; } @Override - public Void structureShape(StructureShape shape) { - defaultComplex(shape); + public Void mapShape(MapShape mapShape) { + Shape valueTarget = model.expectShape(mapShape.getValue().getTarget()); + + writer + .write("") + .openBlock("module $L", symbolProvider.toSymbol(mapShape).getName()) + .openBlock("def self.build(params, context: '')") + .write("$T.validate_types!(params, ::Hash, context: context)", Hearth.VALIDATOR) + .write("data = {}") + .openBlock("params.each do |key, value|") + .call(() -> valueTarget + .accept(new MemberBuilder(model, writer, context.symbolProvider(), "data[key] = ", + "value", "\"#{context}[:#{key}]\"", mapShape.getValue(), + !mapShape.hasTrait(SparseTrait.class)))) + .closeBlock("end") + .write("data") + .closeBlock("end") + .closeBlock("end"); return null; } @Override public Void unionShape(UnionShape shape) { - defaultComplex(shape); + String name = symbolProvider.toSymbol(shape).getName(); + Symbol typeSymbol = context.symbolProvider().toSymbol(shape); + + writer + .write("") + .openBlock("module $L", name) + .openBlock("def self.build(params, context: '')") + .write("return params if params.is_a?($T)", typeSymbol) + .write("$T.validate_types!(params, ::Hash, $T, context: context)", + Hearth.VALIDATOR, typeSymbol) + .openBlock("unless params.size == 1") + .write("raise ArgumentError,") + .indent(3) + .write("\"Expected #{context} to have exactly one member, got: #{params}\"") + .dedent(3) + .closeBlock("end") + .write("key, value = params.flatten") + .write("case key"); //start a case statement. This does NOT indent + + for (MemberShape member : shape.members()) { + Shape target = model.expectShape(member.getTarget()); + String memberClassName = symbolProvider.toMemberName(member); + String memberName = RubyFormatter.asSymbol(memberClassName); + writer.write("when $L", memberName) + .indent() + .openBlock("$T.new(", context.symbolProvider().toSymbol(member)); + String input = "params[" + memberName + "]"; + String contextString = "\"#{context}[" + memberName + "]\""; + target.accept(new MemberBuilder(model, writer, symbolProvider, "", input, contextString, + member, false)); + writer.closeBlock(")") + .dedent(); + } + String expectedMembers = + shape.members().stream().map((member) -> RubyFormatter.asSymbol(member.getMemberName())) + .collect(Collectors.joining(", ")); + writer.write("else") + .indent() + .write("raise ArgumentError,") + .indent(3) + .write("\"Expected #{context} to have one of $L set\"", expectedMembers) + .dedent(4); + writer.write("end") //end of case statement, NOT indented + .closeBlock("end") + .closeBlock("end"); return null; } - private void defaultComplex(Shape shape) { - if (defaultValue.isPresent()) { - if (checkRequired) { - writer.write("$1L$2L.build(params.fetch($3L, $5L), context: $4L)", - memberSetter, symbolProvider.toSymbol(shape).getName(), rubySymbol, context, - defaultValue.get()); + private boolean isComplexShape(Shape shape) { + return shape.isStructureShape() || shape.isListShape() || shape.isMapShape() + || shape.isUnionShape() || shape.isOperationShape(); + } + + private static class MemberBuilder extends ShapeVisitor.Default { + private final Model model; + private final RubyCodeWriter writer; + private final SymbolProvider symbolProvider; + private final String memberSetter; + private final String input; + private final String context; + private final MemberShape memberShape; + private final Optional defaultValue; + private final boolean checkRequired; + private final String rubySymbol; + + MemberBuilder( + Model model, + RubyCodeWriter writer, + SymbolProvider symbolProvider, + String memberSetter, + String input, + String context, + MemberShape memberShape, + boolean checkRequired + ) { + this.model = model; + this.writer = writer; + this.symbolProvider = symbolProvider; + this.memberSetter = memberSetter; + this.input = input; + this.context = context; + this.memberShape = memberShape; + this.checkRequired = checkRequired; + this.rubySymbol = RubyFormatter.asSymbol(symbolProvider.toMemberName(memberShape)); + + // Note: No need to check for box trait for V1 Smithy models. + // Smithy convert V1 to V2 model and populate Default trait automatically + boolean containsRequiredAndDefaultTraits = + memberShape.hasTrait(DefaultTrait.class) + && !memberShape.expectTrait(DefaultTrait.class).toNode().isNullNode() + && memberShape.hasTrait(RequiredTrait.class); + + if (containsRequiredAndDefaultTraits) { + Shape targetShape = model.expectShape(memberShape.getTarget()); + this.defaultValue = Optional.of(targetShape.accept(new DefaultValueRetriever(model, memberShape))); } else { - writer.write("$1L($2L.build(params.fetch($3L, $5L), context: $4L))", - memberSetter, symbolProvider.toSymbol(shape).getName(), rubySymbol, context, - defaultValue.get()); + this.defaultValue = Optional.empty(); } - return; } - if (checkRequired) { - writer.write("$1L$2L.build($3L, context: $4L) unless $3L.nil?", memberSetter, - symbolProvider.toSymbol(shape).getName(), input, context); - } else { - writer.write("$1L($2L.build($3L, context: $4L) unless $3L.nil?)", memberSetter, - symbolProvider.toSymbol(shape).getName(), input, context); + @Override + protected Void getDefault(Shape shape) { + if (defaultValue.isPresent()) { + writer.write("$1Lparams.fetch($2L, $3L)", memberSetter, rubySymbol, defaultValue.get()); + } else { + writer.write("$L$L", memberSetter, input); + } + return null; } - } - } - /** - * Default value constrains: - * enum: can be set to any valid string value of the enum. - * intEnum: can be set to any valid integer value of the enum. - * document: can be set to null, `true, false, string, numbers, an empty list, or an empty map. - * list: can only be set to an empty list. - * map: can only be set to an empty map. - * structure: no default value. - * union: no default value. - * - * See https://awslabs.github.io/smithy/2.0/spec/type-refinement-traits.html?highlight=required#default-value-constraints - */ - private static final class DefaultValueRetriever extends ShapeVisitor.Default { - - private final MemberShape memberShape; - private final Node defaultNode; - private final Model model; - - private DefaultValueRetriever(Model model, MemberShape memberShape) { - this.model = model; - this.memberShape = memberShape; - this.defaultNode = memberShape.expectTrait(DefaultTrait.class).toNode(); - } + @Override + public Void blobShape(BlobShape shape) { + if (shape.hasTrait(StreamingTrait.class)) { + writer + .write("io = $L || StringIO.new", input) + .openBlock("unless io.respond_to?(:read) " + + "|| io.respond_to?(:readpartial)") + .write("io = StringIO.new(io)") + .closeBlock("end") + .write("$Lio", memberSetter); + } else { + getDefault(shape); + } + return null; + } - @Override - protected String getDefault(Shape shape) { - return "nil"; - } + @Override + public Void stringShape(StringShape shape) { + if (memberShape.hasTrait(IdempotencyTokenTrait.class) + || shape.hasTrait(IdempotencyTokenTrait.class)) { + writer.write("$L$L || $T.uuid", memberSetter, input, RubyImportContainer.SECURE_RANDOM); + } else { + getDefault(shape); + } + return null; + } - @Override - public String blobShape(BlobShape shape) { - return getDefaultString(); - } + @Override + public Void listShape(ListShape shape) { + defaultComplex(shape); + return null; + } - @Override - public String booleanShape(BooleanShape shape) { - return getDefaultBoolean(); - } + @Override + public Void mapShape(MapShape shape) { + defaultComplex(shape); + return null; + } - @Override - public String stringShape(StringShape shape) { - return getDefaultString(); - } + @Override + public Void structureShape(StructureShape shape) { + defaultComplex(shape); + return null; + } - @Override - public String byteShape(ByteShape shape) { - return String.valueOf(getDefaultNumber().byteValue()); - } + @Override + public Void unionShape(UnionShape shape) { + defaultComplex(shape); + return null; + } - @Override - public String shortShape(ShortShape shape) { - return String.valueOf(getDefaultNumber().shortValue()); - } + private void defaultComplex(Shape shape) { + if (defaultValue.isPresent()) { + if (checkRequired) { + writer.write("$1L$2L.build(params.fetch($3L, $5L), context: $4L)", + memberSetter, symbolProvider.toSymbol(shape).getName(), rubySymbol, context, + defaultValue.get()); + } else { + writer.write("$1L($2L.build(params.fetch($3L, $5L), context: $4L))", + memberSetter, symbolProvider.toSymbol(shape).getName(), rubySymbol, context, + defaultValue.get()); + } + return; + } - @Override - public String integerShape(IntegerShape shape) { - return String.valueOf(getDefaultNumber().intValue()); + if (checkRequired) { + writer.write("$1L$2L.build($3L, context: $4L) unless $3L.nil?", memberSetter, + symbolProvider.toSymbol(shape).getName(), input, context); + } else { + writer.write("$1L($2L.build($3L, context: $4L) unless $3L.nil?)", memberSetter, + symbolProvider.toSymbol(shape).getName(), input, context); + } + } } - @Override - public String longShape(LongShape shape) { - return String.valueOf(getDefaultNumber().longValue()); - } + /** + * Default value constrains: + * enum: can be set to any valid string value of the enum. + * intEnum: can be set to any valid integer value of the enum. + * document: can be set to null, `true, false, string, numbers, an empty list, or an empty map. + * list: can only be set to an empty list. + * map: can only be set to an empty map. + * structure: no default value. + * union: no default value. + *

+ * See https://awslabs.github.io/smithy/2.0/spec/type-refinement-traits.html?highlight=required#default-value-constraints + */ + private static final class DefaultValueRetriever extends ShapeVisitor.Default { + + private final MemberShape memberShape; + private final Node defaultNode; + private final Model model; + + private DefaultValueRetriever(Model model, MemberShape memberShape) { + this.model = model; + this.memberShape = memberShape; + this.defaultNode = memberShape.expectTrait(DefaultTrait.class).toNode(); + } - @Override - public String floatShape(FloatShape shape) { - return String.valueOf(getDefaultNumber().shortValue()); - } + @Override + protected String getDefault(Shape shape) { + return "nil"; + } - @Override - public String doubleShape(DoubleShape shape) { - return String.valueOf(getDefaultNumber().doubleValue()); - } + @Override + public String blobShape(BlobShape shape) { + return getDefaultString(); + } - @Override - public String bigIntegerShape(BigIntegerShape shape) { - return String.valueOf(getDefaultNumber().intValue()); - } + @Override + public String booleanShape(BooleanShape shape) { + return getDefaultBoolean(); + } - @Override - public String bigDecimalShape(BigDecimalShape shape) { - return String.valueOf(getDefaultNumber().floatValue()); - } + @Override + public String stringShape(StringShape shape) { + return getDefaultString(); + } - @Override - public String enumShape(EnumShape shape) { - return shape.getEnumValues().get(getDefaultString()); - } + @Override + public String byteShape(ByteShape shape) { + return String.valueOf(getDefaultNumber().byteValue()); + } - @Override - public String intEnumShape(IntEnumShape shape) { - return String.valueOf(getDefaultNumber().intValue()); - } + @Override + public String shortShape(ShortShape shape) { + return String.valueOf(getDefaultNumber().shortValue()); + } - @Override - public String listShape(ListShape shape) { - if (defaultNode.asArrayNode().isPresent()) { - return "[]"; + @Override + public String integerShape(IntegerShape shape) { + return String.valueOf(getDefaultNumber().intValue()); } - return "nil"; - } - @Override - public String mapShape(MapShape shape) { - if (defaultNode.asObjectNode().isPresent()) { - return "{}"; + @Override + public String longShape(LongShape shape) { + return String.valueOf(getDefaultNumber().longValue()); } - return "nil"; - } - @Override - public String documentShape(DocumentShape shape) { - if (defaultNode.asNumberNode().isPresent()) { - return getDefaultNumber().toString(); + @Override + public String floatShape(FloatShape shape) { + return String.valueOf(getDefaultNumber().shortValue()); } - if (defaultNode.asBooleanNode().isPresent()) { - return getDefaultBoolean(); + @Override + public String doubleShape(DoubleShape shape) { + return String.valueOf(getDefaultNumber().doubleValue()); } - if (defaultNode.asStringNode().isPresent()) { - return getDefaultString(); + @Override + public String bigIntegerShape(BigIntegerShape shape) { + return String.valueOf(getDefaultNumber().intValue()); } - if (defaultNode.asArrayNode().isPresent()) { - return "[]"; + @Override + public String bigDecimalShape(BigDecimalShape shape) { + return String.valueOf(getDefaultNumber().floatValue()); } - if (defaultNode.asObjectNode().isPresent()) { - return "{}"; + @Override + public String enumShape(EnumShape shape) { + return shape.getEnumValues().get(getDefaultString()); } - return "nil"; - } + @Override + public String intEnumShape(IntEnumShape shape) { + return String.valueOf(getDefaultNumber().intValue()); + } - @Override - public String timestampShape(TimestampShape shape) { - if (defaultNode.isStringNode()) { - return getDefaultString(); - } else if (defaultNode.isNumberNode()) { - return String.valueOf(getDefaultNumber()); - } else { + @Override + public String listShape(ListShape shape) { + if (defaultNode.asArrayNode().isPresent()) { + return "[]"; + } return "nil"; } - } - private String getDefaultString() { - return String.format("\"%s\"", defaultNode.expectStringNode().getValue()); - } + @Override + public String mapShape(MapShape shape) { + if (defaultNode.asObjectNode().isPresent()) { + return "{}"; + } + return "nil"; + } - private String getDefaultBoolean() { - return String.valueOf(defaultNode.expectBooleanNode().getValue()); - } + @Override + public String documentShape(DocumentShape shape) { + if (defaultNode.asNumberNode().isPresent()) { + return getDefaultNumber().toString(); + } + + if (defaultNode.asBooleanNode().isPresent()) { + return getDefaultBoolean(); + } - private Number getDefaultNumber() { - return defaultNode.expectNumberNode().getValue(); + if (defaultNode.asStringNode().isPresent()) { + return getDefaultString(); + } + + if (defaultNode.asArrayNode().isPresent()) { + return "[]"; + } + + if (defaultNode.asObjectNode().isPresent()) { + return "{}"; + } + + return "nil"; + } + + @Override + public String timestampShape(TimestampShape shape) { + if (defaultNode.isStringNode()) { + return getDefaultString(); + } else if (defaultNode.isNumberNode()) { + return String.valueOf(getDefaultNumber()); + } else { + return "nil"; + } + } + + private String getDefaultString() { + return String.format("\"%s\"", defaultNode.expectStringNode().getValue()); + } + + private String getDefaultBoolean() { + return String.valueOf(defaultNode.expectBooleanNode().getValue()); + } + + private Number getDefaultNumber() { + return defaultNode.expectNumberNode().getValue(); + } } } } diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParserGeneratorBase.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParserGeneratorBase.java index 4738d5ca5..aa550c11a 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParserGeneratorBase.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ParserGeneratorBase.java @@ -220,6 +220,7 @@ public void render(FileManifest fileManifest) { .includePreamble() .includeRequires() .openBlock("module $L", settings.getModule()) + .apiPrivate() .openBlock("module Parsers") .call(() -> renderParsers()) .closeBlock("end") diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RestBuilderGeneratorBase.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RestBuilderGeneratorBase.java index 59b8e9226..6f834194e 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RestBuilderGeneratorBase.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RestBuilderGeneratorBase.java @@ -187,7 +187,7 @@ protected void renderQueryInputBuilder(Shape inputShape) { LOGGER.finest("Generated query input builder for " + m.getMemberName()); } - writer.write("http_req.append_query_params(params)"); + writer.write("http_req.append_query_param_list(params)"); } /** @@ -214,7 +214,7 @@ protected void renderHeadersBuilder(Shape inputShape) { * @param inputShape inputShape to render for */ protected void renderPrefixHeadersBuilder(Shape inputShape) { - // get a list of all of HttpLabel members + // get a list of all of HttpPrefixHeaders members List headerMembers = inputShape.members() .stream() .filter((m) -> m.hasTrait(HttpPrefixHeadersTrait.class)) @@ -230,7 +230,7 @@ protected void renderPrefixHeadersBuilder(Shape inputShape) { String symbolName = ":" + symbolProvider.toMemberName(m); String headerSetter = "http_req.headers[\"" + prefix + "#{key}\"] = "; writer - .openBlock("input[$L].each do |key, value|", symbolName) + .openBlock("input[$L]&.each do |key, value|", symbolName) .call(() -> valueShape.accept(new HeaderSerializer(m, headerSetter, "value"))) .closeBlock("end"); LOGGER.finest("Generated prefix header builder for " + m.getMemberName()); @@ -248,7 +248,7 @@ protected void renderUriBuilder(OperationShape operation, Shape inputShape) { String[] uriParts = uri.split("[?]"); if (uriParts.length > 1) { uri = uriParts[0]; - // TODO this should use append_query_params? interface needs to be changed in Hearth if so + // TODO this should use append_query_param_list? interface needs to be changed in Hearth if so writer .openBlock("CGI.parse('$L').each do |k,v|", uriParts[1]) .write("v.each { |q_v| http_req.append_query_param(k, q_v) }") @@ -396,15 +396,7 @@ public Void timestampShape(TimestampShape shape) { @Override public Void listShape(ListShape shape) { - writer.openBlock("unless $1L.nil? || $1L.empty?", inputGetter) - .write("$1L$2L", dataSetter, inputGetter) - .indent() - .write(".compact") - .call(() -> model.expectShape(shape.getMember().getTarget()) - .accept(new HeaderListMemberSerializer(shape.getMember()))) - .write(".join(', ')") - .dedent() - .closeBlock("end"); + writer.write("$1L$2L unless $2L.nil? || $2L.empty?", dataSetter, inputGetter); return null; } @@ -427,37 +419,6 @@ public Void unionShape(UnionShape shape) { } } - protected class HeaderListMemberSerializer extends ShapeVisitor.Default { - - private final MemberShape memberShape; - - HeaderListMemberSerializer(MemberShape memberShape) { - this.memberShape = memberShape; - } - - @Override - protected Void getDefault(Shape shape) { - writer.write(".map { |s| s.to_s }"); - return null; - } - - @Override - public Void stringShape(StringShape shape) { - writer - .write(".map { |s| (s.include?('\"') || s.include?(\",\"))" - + " ? \"\\\"#{s.gsub('\"', '\\\"')}\\\"\" : s }"); - return null; - } - - @Override - public Void timestampShape(TimestampShape shape) { - writer.write(".map { |s| $L }", - TimestampFormat.serializeTimestamp( - shape, memberShape, "s", TimestampFormatTrait.Format.HTTP_DATE, false)); - return null; - } - } - protected class LabelMemberSerializer extends ShapeVisitor.Default { private final MemberShape memberShape; diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RestStubsGeneratorBase.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RestStubsGeneratorBase.java index 6230b7fd8..6f8f625e1 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RestStubsGeneratorBase.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RestStubsGeneratorBase.java @@ -86,7 +86,7 @@ public RestStubsGeneratorBase(GenerationContext context) { * http_resp.status = 200 * #### START code generated by this method * http_resp.headers['Content-Type'] = 'application/octet-stream' - * http_resp.body = StringIO.new(stub[:blob] || '') + * http_resp.body.write(stub[:blob] || '') * #### END code generated by this method * end * end @@ -125,7 +125,7 @@ protected abstract void renderPayloadBodyStub(OperationShape operation, Shape ou * ### START code generated by this method * data[:value] = stub[:value] unless stub[:value].nil? * #### END code generated by this method - * http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + * http_resp.body.write(Hearth::JSON.dump(data)) * end * end * } diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RubyGeneratorBase.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RubyGeneratorBase.java new file mode 100644 index 000000000..3e10acf3c --- /dev/null +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/RubyGeneratorBase.java @@ -0,0 +1,67 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.ruby.codegen.generators; + +import java.util.function.Consumer; +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.codegen.core.directed.ContextualDirective; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.ruby.codegen.GenerationContext; +import software.amazon.smithy.ruby.codegen.RubyCodeWriter; +import software.amazon.smithy.ruby.codegen.RubySettings; + +abstract class RubyGeneratorBase { + final Model model; + + final SymbolProvider symbolProvider; + + final RubySettings settings; + + final GenerationContext context; + + RubyGeneratorBase(ContextualDirective directive) { + this.symbolProvider = directive.symbolProvider(); + this.settings = directive.settings(); + this.context = directive.context(); + this.model = directive.model(); + } + + abstract String getModule(); + + public final void write(Consumer writerConsumer) { + write(rbFile(), nameSpace(), writerConsumer); + } + + public final void writeRbs(Consumer writerConsumer) { + write(rbsFile(), nameSpace(), writerConsumer); + } + + public final void write(String file, String namespace, Consumer writerConsumer) { + context.writerDelegator().useFileWriter(file, namespace, writerConsumer); + } + + public String nameSpace() { + return settings.getModule() + "::" + getModule(); + } + + public String rbFile() { + return settings.getGemName() + "/lib/" + settings.getGemName() + "/" + getModule().toLowerCase() + ".rb"; + } + + public String rbsFile() { + return settings.getGemName() + "/sig/" + settings.getGemName() + "/" + getModule().toLowerCase() + ".rbs"; + } +} diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/StructureGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/StructureGenerator.java new file mode 100644 index 000000000..98b47c99d --- /dev/null +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/StructureGenerator.java @@ -0,0 +1,194 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.ruby.codegen.generators; + +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.codegen.core.directed.ShapeDirective; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.NullableIndex; +import software.amazon.smithy.model.shapes.BooleanShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeVisitor; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.SensitiveTrait; +import software.amazon.smithy.ruby.codegen.GenerationContext; +import software.amazon.smithy.ruby.codegen.Hearth; +import software.amazon.smithy.ruby.codegen.RubyCodeWriter; +import software.amazon.smithy.ruby.codegen.RubyFormatter; +import software.amazon.smithy.ruby.codegen.RubySettings; +import software.amazon.smithy.ruby.codegen.generators.docs.ShapeDocumentationGenerator; +import software.amazon.smithy.utils.SmithyInternalApi; + +@SmithyInternalApi +public final class StructureGenerator extends RubyGeneratorBase { + + private final StructureShape shape; + + public StructureGenerator(ShapeDirective directive) { + super(directive); + this.shape = directive.shape(); + } + + @Override + String getModule() { + return "Types"; + } + + public void render() { + write(writer -> { + String membersBlock = "nil"; + if (!shape.members().isEmpty()) { + membersBlock = shape + .members() + .stream() + .map(memberShape -> RubyFormatter.asSymbol(symbolProvider.toMemberName(memberShape))) + .collect(Collectors.joining(",\n")); + } + membersBlock += ","; + + String documentation = new ShapeDocumentationGenerator(model, symbolProvider, shape).render(); + + writer.writeInline("$L", documentation); + + shape.members().forEach(memberShape -> { + String attribute = symbolProvider.toMemberName(memberShape); + Shape target = model.expectShape(memberShape.getTarget()); + String returnType = (String) symbolProvider.toSymbol(target) + .getProperty("yardType").orElseThrow(IllegalArgumentException::new); + + String memberDocumentation = + new ShapeDocumentationGenerator(model, symbolProvider, memberShape).render(); + + writer.writeYardAttribute(attribute, () -> { + // delegate to member shape in this visitor + writer.writeInline("$L", memberDocumentation); + writer.writeYardReturn(returnType, ""); + }); + }); + + writer + .openBlock("$T = ::Struct.new(", symbolProvider.toSymbol(shape)) + .write(membersBlock) + .write("keyword_init: true") + .closeBlock(") do") + .indent() + .write("include $T", Hearth.STRUCTURE) + .call(() -> renderStructureInitializeMethod(writer, model, shape)) + .call(() -> renderStructureToSMethod(writer, model, shape)) + .closeBlock("end\n"); + }); + + writeRbs(writer -> { + Symbol symbol = symbolProvider.toSymbol(shape); + String shapeName = symbol.getName(); + writer.write(shapeName + ": untyped\n"); + }); + } + + private void renderStructureInitializeMethod( + RubyCodeWriter writer, + Model model, + StructureShape structureShape + ) { + NullableIndex nullableIndex = new NullableIndex(model); + List defaultMembers = structureShape.members().stream() + .filter((m) -> !nullableIndex.isNullable(m)) + .toList(); + + if (!defaultMembers.isEmpty()) { + writer + .openBlock("\ndef initialize(*)") + .write("super") + .call(() -> { + defaultMembers.forEach((m) -> { + String attribute = symbolProvider.toMemberName(m); + Shape target = model.expectShape(m.getTarget()); + + writer.write("self.$L ||= $L", + attribute, + target.accept(new MemberDefaultVisitor())); + }); + }) + .closeBlock("end"); + } + } + + private void renderStructureToSMethod( + RubyCodeWriter writer, + Model model, + StructureShape structureShape + ) { + String fullQualifiedShapeName = settings.getModule() + "::Types::" + + symbolProvider.toSymbol(structureShape).getName(); + + boolean hasSensitiveMember = structureShape.members().stream() + .anyMatch(memberShape -> memberShape.getMemberTrait(model, SensitiveTrait.class).isPresent()); + + if (structureShape.hasTrait(SensitiveTrait.class)) { + // structure is itself sensitive + writer + .openBlock("\ndef to_s") + .write("\"#\"", fullQualifiedShapeName) + .closeBlock("end"); + } else if (hasSensitiveMember) { + // at least one member is sensitive + Iterator iterator = structureShape.members().iterator(); + + writer + .openBlock("\ndef to_s") + .write("\"#\"", key, value); + } + } + writer + .dedent() + .closeBlock("end"); + } + } + + private static class MemberDefaultVisitor extends ShapeVisitor.Default { + @Override + protected String getDefault(Shape shape) { + return "0"; + } + + @Override + public String booleanShape(BooleanShape s) { + return "false"; + } + } +} diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/StubsGeneratorBase.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/StubsGeneratorBase.java index 64a7c6d17..6d42a76db 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/StubsGeneratorBase.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/StubsGeneratorBase.java @@ -185,7 +185,7 @@ public StubsGeneratorBase(GenerationContext context) { * http_resp.status = 200 * http_resp.headers['Content-Type'] = 'application/json' * data['contents'] = Stubs::Contents.stub(stub[:contents]) unless stub[:contents].nil? - * http_resp.body = StringIO.new(Hearth::JSON.dump(data)) + * http_resp.body.write(Hearth::JSON.dump(data)) * end * #### END code generated by this method * } @@ -202,6 +202,7 @@ public void render(FileManifest fileManifest) { .includePreamble() .includeRequires() .openBlock("module $L", settings.getModule()) + .apiPrivate() .openBlock("module Stubs") .call(() -> renderStubs()) .closeBlock("end") diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesFileBlockGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesFileBlockGenerator.java new file mode 100644 index 000000000..11d208ab4 --- /dev/null +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesFileBlockGenerator.java @@ -0,0 +1,53 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.ruby.codegen.generators; + +import software.amazon.smithy.codegen.core.directed.ContextualDirective; +import software.amazon.smithy.ruby.codegen.GenerationContext; +import software.amazon.smithy.ruby.codegen.RubyCodeWriter; +import software.amazon.smithy.ruby.codegen.RubySettings; +import software.amazon.smithy.utils.SmithyInternalApi; + +@SmithyInternalApi +public class TypesFileBlockGenerator extends RubyGeneratorBase { + + public TypesFileBlockGenerator(ContextualDirective directive) { + super(directive); + } + + @Override + String getModule() { + return "Types"; + } + + public void openBlocks() { + context.writerDelegator().useFileWriter(rbFile(), nameSpace(), writer -> { + writer.includePreamble().includeRequires(); + writer.addModule(settings.getModule()); + writer.addModule("Types"); + }); + context.writerDelegator().useFileWriter(rbsFile(), nameSpace(), writer -> { + writer.includePreamble(); + writer.addModule(settings.getModule()); + writer.addModule("Types"); + }); + } + + public void closeAllBlocks() { + context.writerDelegator().useFileWriter(rbFile(), nameSpace(), RubyCodeWriter::closeAllModules); + context.writerDelegator().useFileWriter(rbsFile(), nameSpace(), RubyCodeWriter::closeAllModules); + } +} diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesGenerator.java deleted file mode 100644 index 24bfcfb40..000000000 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/TypesGenerator.java +++ /dev/null @@ -1,495 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.smithy.ruby.codegen.generators; - -import java.util.Comparator; -import java.util.Iterator; -import java.util.List; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import software.amazon.smithy.build.FileManifest; -import software.amazon.smithy.codegen.core.Symbol; -import software.amazon.smithy.codegen.core.SymbolProvider; -import software.amazon.smithy.model.Model; -import software.amazon.smithy.model.knowledge.NullableIndex; -import software.amazon.smithy.model.neighbor.Walker; -import software.amazon.smithy.model.shapes.BooleanShape; -import software.amazon.smithy.model.shapes.EnumShape; -import software.amazon.smithy.model.shapes.IntEnumShape; -import software.amazon.smithy.model.shapes.MemberShape; -import software.amazon.smithy.model.shapes.Shape; -import software.amazon.smithy.model.shapes.ShapeVisitor; -import software.amazon.smithy.model.shapes.StringShape; -import software.amazon.smithy.model.shapes.StructureShape; -import software.amazon.smithy.model.shapes.UnionShape; -import software.amazon.smithy.model.traits.EnumDefinition; -import software.amazon.smithy.model.traits.EnumTrait; -import software.amazon.smithy.model.traits.SensitiveTrait; -import software.amazon.smithy.model.transform.ModelTransformer; -import software.amazon.smithy.ruby.codegen.GenerationContext; -import software.amazon.smithy.ruby.codegen.Hearth; -import software.amazon.smithy.ruby.codegen.RubyCodeWriter; -import software.amazon.smithy.ruby.codegen.RubyFormatter; -import software.amazon.smithy.ruby.codegen.RubySettings; -import software.amazon.smithy.ruby.codegen.generators.docs.ShapeDocumentationGenerator; -import software.amazon.smithy.utils.SmithyInternalApi; - -@SmithyInternalApi -public class TypesGenerator { - private static final Logger LOGGER = - Logger.getLogger(TypesGenerator.class.getName()); - - private final GenerationContext context; - private final RubySettings settings; - private final Model model; - private final RubyCodeWriter writer; - private final RubyCodeWriter rbsWriter; - private final SymbolProvider symbolProvider; - - public TypesGenerator(GenerationContext context) { - this.context = context; - this.settings = context.settings(); - this.model = context.model(); - this.writer = new RubyCodeWriter(getNameSpace()); - this.rbsWriter = new RubyCodeWriter(getNameSpace()); - this.symbolProvider = context.symbolProvider(); - } - - public TypesVisitor getTypeVisitor(RubyCodeWriter writer) { - return new TypesVisitor(writer); - } - - public String getFile() { - return settings.getGemName() + "/lib/" + settings.getGemName() + "/types.rb"; - } - - public String getNameSpace() { - return context.settings().getModule() + "::Types"; - } - - public void render() { - FileManifest fileManifest = context.fileManifest(); - - writer - .includePreamble() - .includeRequires() - .openBlock("module $L", settings.getModule()) - .openBlock("module Types") - .write("") - .call(() -> renderTypes(getTypeVisitor(writer))) - .closeBlock("end") - .closeBlock("end"); - - String fileName = getFile(); - fileManifest.writeFile(fileName, writer.toString()); - LOGGER.fine("Wrote types to " + fileName); - } - - public void renderRbs() { - FileManifest fileManifest = context.fileManifest(); - - rbsWriter - .includePreamble() - .openBlock("module $L", settings.getModule()) - .openBlock("module Types") - .write("") - .call(() -> renderTypes(new TypesRbsVisitor())) - .closeBlock("end") - .closeBlock("end"); - - String fileName = - settings.getGemName() + "/sig/" + settings.getGemName() - + "/types.rbs"; - fileManifest.writeFile(fileName, rbsWriter.toString()); - LOGGER.fine("Wrote types rbs to " + fileName); - } - - public void renderTypes(ShapeVisitor visitor) { - Model modelWithoutTraitShapes = ModelTransformer.create() - .getModelWithoutTraitShapes(model); - - new Walker(modelWithoutTraitShapes) - .walkShapes(context.service()) - .stream() - .sorted(Comparator.comparing((o) -> o.getId().getName())) - .forEach((shape) -> shape.accept(visitor)); - } - - public class TypesVisitor extends ShapeVisitor.Default { - - private RubyCodeWriter writer; - - public TypesVisitor(RubyCodeWriter writer) { - this.writer = writer; - } - - @Override - protected Void getDefault(Shape shape) { - return null; - } - - @Override - public Void structureShape(StructureShape shape) { - String membersBlock = "nil"; - if (!shape.members().isEmpty()) { - membersBlock = shape - .members() - .stream() - .map(memberShape -> RubyFormatter.asSymbol(symbolProvider.toMemberName(memberShape))) - .collect(Collectors.joining(",\n")); - } - membersBlock += ","; - - String documentation = new ShapeDocumentationGenerator(model, symbolProvider, shape).render(); - - writer.writeInline("$L", documentation); - - shape.members().forEach(memberShape -> { - String attribute = symbolProvider.toMemberName(memberShape); - Shape target = model.expectShape(memberShape.getTarget()); - String returnType = (String) symbolProvider.toSymbol(target) - .getProperty("yardType").orElseThrow(IllegalArgumentException::new); - - String memberDocumentation = - new ShapeDocumentationGenerator(model, symbolProvider, memberShape).render(); - - writer.writeYardAttribute(attribute, () -> { - // delegate to member shape in this visitor - writer.writeInline("$L", memberDocumentation); - writer.writeYardReturn(returnType, ""); - }); - }); - - writer - .openBlock("$T = ::Struct.new(", symbolProvider.toSymbol(shape)) - .write(membersBlock) - .write("keyword_init: true") - .closeBlock(") do") - .indent() - .write("include $T", Hearth.STRUCTURE) - .call(() -> renderStructureInitializeMethod(shape)) - .call(() -> renderStructureToSMethod(shape)) - .closeBlock("end\n"); - - return null; - } - - @Override - public Void unionShape(UnionShape shape) { - String documentation = new ShapeDocumentationGenerator(model, symbolProvider, shape).render(); - - writer.writeInline("$L", documentation); - writer.openBlock("class $T < $T", symbolProvider.toSymbol(shape), Hearth.UNION); - - for (MemberShape memberShape : shape.members()) { - String memberDocumentation = - new ShapeDocumentationGenerator(model, symbolProvider, memberShape).render(); - - writer - .writeInline("$L", memberDocumentation) - .openBlock("class $L < $T", - symbolProvider.toMemberName(memberShape), symbolProvider.toSymbol(shape)) - .openBlock("def to_h") - .write("{ $L: super(__getobj__) }", - RubyFormatter.toSnakeCase(symbolProvider.toMemberName(memberShape))) - .closeBlock("end") - .call(() -> renderUnionToSMethod(memberShape)) - .closeBlock("end\n"); - } - - writer - .writeDocstring("Handles unknown future members") - .openBlock("class Unknown < $T", symbolProvider.toSymbol(shape)) - .openBlock("def to_h") - .write("{ unknown: super(__getobj__) }") - .closeBlock("end\n") - .openBlock("def to_s") - .write("\"#<$L::Types::Unknown #{__getobj__ || 'nil'}>\"", settings.getModule()) - .closeBlock("end") - .closeBlock("end") - .closeBlock("end\n"); - - return null; - } - - @Override - public Void stringShape(StringShape shape) { - // Only write out string shapes for enums - if (shape.hasTrait(EnumTrait.class)) { - EnumTrait enumTrait = shape.expectTrait(EnumTrait.class); - List enumDefinitions = enumTrait.getValues().stream() - .filter(value -> value.getName().isPresent()) - .collect(Collectors.toList()); - - // only write out a module if there is at least one enum constant - if (enumDefinitions.size() > 0) { - String shapeName = symbolProvider.toSymbol(shape).getName(); - - writer - .writeDocstring("Includes enum constants for " + shapeName) - .openBlock("module $L", shapeName); - - enumDefinitions.forEach(enumDefinition -> { - String enumName = enumDefinition.getName().get(); - String enumValue = enumDefinition.getValue(); - String enumDocumentation = enumDefinition.getDocumentation() - .orElse("No documentation available."); - writer.writeDocstring(enumDocumentation); - if (enumDefinition.isDeprecated()) { - writer.writeYardDeprecated("This enum value is deprecated.", ""); - } - if (!enumDefinition.getTags().isEmpty()) { - String enumTags = enumDefinition.getTags().stream() - .map((tag) -> "\"" + tag + "\"") - .collect(Collectors.joining(", ")); - writer.writeDocstring("Tags: [" + enumTags + "]"); - } - writer.write("$L = $S\n", enumName, enumValue); - }); - - writer - .unwrite("\n") - .closeBlock("end\n"); - } - } - - return null; - } - - @Override - public Void enumShape(EnumShape shape) { - EnumTrait enumTrait = shape.expectTrait(EnumTrait.class); - List enumDefinitions = enumTrait.getValues().stream() - .filter(value -> value.getName().isPresent()) - .collect(Collectors.toList()); - - // only write out a module if there is at least one enum constant - if (enumDefinitions.size() > 0) { - String shapeName = symbolProvider.toSymbol(shape).getName(); - - writer - .writeDocstring("Includes enum constants for " + shapeName) - .openBlock("module $L", shapeName); - - enumDefinitions.forEach(enumDefinition -> { - String enumName = enumDefinition.getName().get(); - String enumValue = enumDefinition.getValue(); - String enumDocumentation = enumDefinition.getDocumentation() - .orElse("No documentation available."); - writer.writeDocstring(enumDocumentation); - if (enumDefinition.isDeprecated()) { - writer.writeYardDeprecated("This enum value is deprecated.", ""); - } - if (!enumDefinition.getTags().isEmpty()) { - String enumTags = enumDefinition.getTags().stream() - .map((tag) -> "\"" + tag + "\"") - .collect(Collectors.joining(", ")); - writer.writeDocstring("Tags: [" + enumTags + "]"); - } - writer.write("$L = $S\n", enumName, enumValue); - }); - - writer - .unwrite("\n") - .closeBlock("end\n"); - } - - return null; - } - - @Override - public Void intEnumShape(IntEnumShape shape) { - // only write out a module if there is at least one enum constant - if (shape.getEnumValues().size() > 0) { - String shapeName = symbolProvider.toSymbol(shape).getName(); - - writer.writeDocstring("Includes enum constants for " + shapeName) - .addModule(shapeName); - - shape.getEnumValues() - .forEach((enumName, enumValue) -> writer.write("$L = $L\n", enumName, enumValue)); - - writer.unwrite("\n").closeModule(); - } - - return null; - } - - private void renderStructureInitializeMethod(StructureShape structureShape) { - NullableIndex nullableIndex = new NullableIndex(model); - List defaultMembers = structureShape.members().stream() - .filter((m) -> !nullableIndex.isNullable(m)) - .collect(Collectors.toList()); - if (!defaultMembers.isEmpty()) { - writer - .openBlock("\ndef initialize(*)") - .write("super") - .call(() -> { - defaultMembers.forEach((m) -> { - String attribute = symbolProvider.toMemberName(m); - Shape target = model.expectShape(m.getTarget()); - writer.write("self.$L ||= $L", - attribute, - target.accept(new MemberDefaultVisitor())); - }); - }) - .closeBlock("end"); - } - } - - private void renderStructureToSMethod(StructureShape structureShape) { - String fullQualifiedShapeName = settings.getModule() + "::Types::" - + symbolProvider.toSymbol(structureShape).getName(); - - Boolean hasSensitiveMember = structureShape.members().stream() - .anyMatch(memberShape -> memberShape.getMemberTrait(model, SensitiveTrait.class).isPresent()); - - if (structureShape.hasTrait(SensitiveTrait.class)) { - // structure is itself sensitive - writer - .openBlock("\ndef to_s") - .write("\"#\"", fullQualifiedShapeName) - .closeBlock("end"); - } else if (hasSensitiveMember) { - // at least one member is sensitive - Iterator iterator = structureShape.members().iterator(); - - writer - .openBlock("\ndef to_s") - .write("\"#\"", key, value); - } - } - writer - .dedent() - .closeBlock("end"); - } - } - - private void renderUnionToSMethod(MemberShape memberShape) { - String fullQualifiedShapeName = settings.getModule() + "::Types::" - + symbolProvider.toMemberName(memberShape); - - writer.write("") - .openBlock("def to_s"); - - if (memberShape.getMemberTrait(model, SensitiveTrait.class).isPresent()) { - writer.write("\"#<$L [SENSITIVE]>\"", fullQualifiedShapeName); - } else { - writer.write("\"#<$L #{__getobj__ || 'nil'}>\"", fullQualifiedShapeName); - } - - writer.closeBlock("end"); - } - } - - private class MemberDefaultVisitor extends ShapeVisitor.Default { - - @Override - protected String getDefault(Shape shape) { - return "0"; - } - - @Override - public String booleanShape(BooleanShape s) { - return "false"; - } - } - - private class TypesRbsVisitor extends ShapeVisitor.Default { - @Override - protected Void getDefault(Shape shape) { - return null; - } - - @Override - public Void structureShape(StructureShape shape) { - Symbol symbol = symbolProvider.toSymbol(shape); - String shapeName = symbol.getName(); - - rbsWriter.write(shapeName + ": untyped\n"); - return null; - } - - @Override - public Void unionShape(UnionShape shape) { - Symbol symbol = symbolProvider.toSymbol(shape); - rbsWriter.openBlock("class $T < $T", symbol, Hearth.UNION); - - for (MemberShape memberShape : shape.members()) { - rbsWriter - .openBlock("class $L < $T", - symbolProvider.toMemberName(memberShape), symbol) - .write("def to_h: () -> { $L: Hash[Symbol,$T] }", - RubyFormatter.toSnakeCase(symbolProvider.toMemberName(memberShape)), - symbol) - .closeBlock("end\n"); - } - - rbsWriter - .openBlock("class Unknown < $T", symbol) - .write("def to_h: () -> { unknown: Hash[Symbol,$T] }", - symbol) - .closeBlock("end") - .closeBlock("end\n"); - - return null; - } - - @Override - public Void stringShape(StringShape shape) { - // Only write out string shapes for enums - if (shape.hasTrait(EnumTrait.class)) { - EnumTrait enumTrait = shape.expectTrait(EnumTrait.class); - List enumDefinitions = enumTrait.getValues().stream() - .filter(value -> value.getName().isPresent()) - .collect(Collectors.toList()); - - // only write out a module if there is at least one enum constant - if (enumDefinitions.size() > 0) { - String shapeName = symbolProvider.toSymbol(shape).getName(); - rbsWriter.openBlock("module $L", shapeName); - enumDefinitions.forEach(enumDefinition -> { - String enumName = enumDefinition.getName().get(); - rbsWriter.write("$L: ::String\n", enumName); - }); - rbsWriter - .unwrite("\n") - .closeBlock("end\n"); - } - } - - return null; - } - } -} diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/UnionGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/UnionGenerator.java new file mode 100644 index 000000000..f73095e9e --- /dev/null +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/UnionGenerator.java @@ -0,0 +1,124 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.ruby.codegen.generators; + +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.codegen.core.directed.GenerateUnionDirective; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.UnionShape; +import software.amazon.smithy.model.traits.SensitiveTrait; +import software.amazon.smithy.ruby.codegen.GenerationContext; +import software.amazon.smithy.ruby.codegen.Hearth; +import software.amazon.smithy.ruby.codegen.RubyCodeWriter; +import software.amazon.smithy.ruby.codegen.RubyFormatter; +import software.amazon.smithy.ruby.codegen.RubySettings; +import software.amazon.smithy.ruby.codegen.generators.docs.ShapeDocumentationGenerator; +import software.amazon.smithy.utils.SmithyInternalApi; + +@SmithyInternalApi +public final class UnionGenerator extends RubyGeneratorBase { + + private final UnionShape shape; + + public UnionGenerator(GenerateUnionDirective directive) { + super(directive); + this.shape = directive.shape(); + } + + @Override + String getModule() { + return "Types"; + } + + public void render() { + String documentation = new ShapeDocumentationGenerator(model, symbolProvider, shape).render(); + + write(writer -> { + writer.writeInline("$L", documentation); + writer.openBlock("class $T < $T", symbolProvider.toSymbol(shape), Hearth.UNION); + + for (MemberShape memberShape : shape.members()) { + String memberDocumentation = + new ShapeDocumentationGenerator(model, symbolProvider, memberShape).render(); + + writer + .writeInline("$L", memberDocumentation) + .openBlock("class $L < $T", + symbolProvider.toMemberName(memberShape), symbolProvider.toSymbol(shape)) + .openBlock("def to_h") + .write("{ $L: super(__getobj__) }", + RubyFormatter.toSnakeCase(symbolProvider.toMemberName(memberShape))) + .closeBlock("end") + .call(() -> renderUnionToSMethod(writer, model, memberShape)) + .closeBlock("end\n"); + } + + writer + .writeDocstring("Handles unknown future members") + .openBlock("class Unknown < $T", symbolProvider.toSymbol(shape)) + .openBlock("def to_h") + .write("{ unknown: super(__getobj__) }") + .closeBlock("end\n") + .openBlock("def to_s") + .write("\"#<$L::Types::Unknown #{__getobj__ || 'nil'}>\"", settings.getModule()) + .closeBlock("end") + .closeBlock("end") + .closeBlock("end\n"); + }); + + writeRbs(writer -> { + Symbol symbol = symbolProvider.toSymbol(shape); + writer.openBlock("class $T < $T", symbol, Hearth.UNION); + + for (MemberShape memberShape : shape.members()) { + writer + .openBlock("class $L < $T", + symbolProvider.toMemberName(memberShape), symbol) + .write("def to_h: () -> { $L: Hash[Symbol,$T] }", + RubyFormatter.toSnakeCase(symbolProvider.toMemberName(memberShape)), + symbol) + .closeBlock("end\n"); + } + + writer + .openBlock("class Unknown < $T", symbol) + .write("def to_h: () -> { unknown: Hash[Symbol,$T] }", + symbol) + .closeBlock("end") + .closeBlock("end\n"); + }); + } + + private void renderUnionToSMethod( + RubyCodeWriter writer, + Model model, + MemberShape memberShape) { + String fullQualifiedShapeName = settings.getModule() + "::Types::" + + symbolProvider.toMemberName(memberShape); + + writer.write("") + .openBlock("def to_s"); + + if (memberShape.getMemberTrait(model, SensitiveTrait.class).isPresent()) { + writer.write("\"#<$L [SENSITIVE]>\"", fullQualifiedShapeName); + } else { + writer.write("\"#<$L #{__getobj__ || 'nil'}>\"", fullQualifiedShapeName); + } + + writer.closeBlock("end"); + } +} diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ValidatorsGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ValidatorsGenerator.java index 658e641de..fe122c1c0 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ValidatorsGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/ValidatorsGenerator.java @@ -18,9 +18,9 @@ import java.util.Collection; import java.util.Comparator; import java.util.logging.Logger; -import software.amazon.smithy.build.FileManifest; import software.amazon.smithy.codegen.core.Symbol; import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.codegen.core.directed.ContextualDirective; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.neighbor.Walker; import software.amazon.smithy.model.shapes.BigDecimalShape; @@ -54,42 +54,36 @@ import software.amazon.smithy.utils.SmithyInternalApi; @SmithyInternalApi -public class ValidatorsGenerator extends ShapeVisitor.Default { +public class ValidatorsGenerator extends RubyGeneratorBase { private static final Logger LOGGER = Logger.getLogger(ValidatorsGenerator.class.getName()); - private final GenerationContext context; - private final RubySettings settings; - private final Model model; - private final RubyCodeWriter writer; - private final SymbolProvider symbolProvider; - - public ValidatorsGenerator(GenerationContext context) { - this.context = context; - this.settings = context.settings(); - this.model = context.model(); - this.writer = new RubyCodeWriter(context.settings().getModule() + "::Validators"); - this.symbolProvider = context.symbolProvider(); + public ValidatorsGenerator(ContextualDirective directive) { + super(directive); + } + + @Override + String getModule() { + return "Validators"; } public void render() { - FileManifest fileManifest = context.fileManifest(); - writer + write(writer -> { + writer .includePreamble() .includeRequires() .openBlock("module $L", settings.getModule()) + .apiPrivate() .openBlock("module Validators") - .call(() -> renderValidators()) + .call(() -> renderValidators(writer)) .write("") .closeBlock("end") .closeBlock("end"); - - String fileName = settings.getGemName() + "/lib/" + settings.getGemName() + "/validators.rb"; - fileManifest.writeFile(fileName, writer.toString()); - LOGGER.fine("Wrote validators to " + fileName); + }); + LOGGER.fine("Wrote validators to " + rbFile()); } - private void renderValidators() { + private void renderValidators(RubyCodeWriter writer) { Model modelWithoutTraitShapes = ModelTransformer.create() .getModelWithoutTraitShapes(model); @@ -97,13 +91,21 @@ private void renderValidators() { .walkShapes(context.service()) .stream() .sorted(Comparator.comparing((o) -> o.getId().getName())) - .forEach((shape) -> shape.accept(this)); + .forEach((shape) -> shape.accept(new Visitor(writer))); } - @Override - public Void structureShape(StructureShape structureShape) { - Collection members = structureShape.members(); - writer + private final class Visitor extends ShapeVisitor.Default { + + private final RubyCodeWriter writer; + + private Visitor(RubyCodeWriter writer) { + this.writer = writer; + } + + @Override + public Void structureShape(StructureShape structureShape) { + Collection members = structureShape.members(); + writer .write("") .openBlock("class $L", symbolProvider.toSymbol(structureShape).getName()) .openBlock("def self.validate!(input, context:)") @@ -114,27 +116,27 @@ public Void structureShape(StructureShape structureShape) { .closeBlock("end") .closeBlock("end"); - return null; - } + return null; + } - private void renderValidatorsForStructureMembers(Collection members) { - members.forEach(member -> { - Shape target = model.expectShape(member.getTarget()); - String symbolName = ":" + symbolProvider.toMemberName(member); - String input = "input[" + symbolName + "]"; - String context = "\"#{context}[" + symbolName + "]\""; - if (member.hasTrait(RequiredTrait.class)) { - writer.write("$T.validate_required!($L, context: $L)", Hearth.VALIDATOR, input, context); - } - target.accept(new MemberValidator(writer, symbolProvider, input, context, false)); - }); - } + private void renderValidatorsForStructureMembers(Collection members) { + members.forEach(member -> { + Shape target = model.expectShape(member.getTarget()); + String symbolName = ":" + symbolProvider.toMemberName(member); + String input = "input[" + symbolName + "]"; + String context = "\"#{context}[" + symbolName + "]\""; + if (member.hasTrait(RequiredTrait.class)) { + writer.write("$T.validate_required!($L, context: $L)", Hearth.VALIDATOR, input, context); + } + target.accept(new MemberValidator(writer, symbolProvider, input, context, false)); + }); + } - @Override - public Void mapShape(MapShape mapShape) { - Shape valueTarget = model.expectShape(mapShape.getValue().getTarget()); + @Override + public Void mapShape(MapShape mapShape) { + Shape valueTarget = model.expectShape(mapShape.getValue().getTarget()); - writer + writer .write("") .openBlock("class $L", symbolProvider.toSymbol(mapShape).getName()) .openBlock("def self.validate!(input, context:)") @@ -147,15 +149,15 @@ public Void mapShape(MapShape mapShape) { .closeBlock("end") .closeBlock("end"); - return null; - } + return null; + } - @Override - public Void listShape(ListShape listShape) { - Shape memberTarget = - model.expectShape(listShape.getMember().getTarget()); + @Override + public Void listShape(ListShape listShape) { + Shape memberTarget = + model.expectShape(listShape.getMember().getTarget()); - writer + writer .write("") .openBlock("class $L", symbolProvider.toSymbol(listShape).getName()) .openBlock("def self.validate!(input, context:)") @@ -167,16 +169,16 @@ public Void listShape(ListShape listShape) { .closeBlock("end") .closeBlock("end"); - return null; - } + return null; + } - @Override - public Void unionShape(UnionShape unionShape) { - Symbol unionType = context.symbolProvider().toSymbol(unionShape); - String shapeName = unionType.getName(); - Collection unionMemberShapes = unionShape.members(); + @Override + public Void unionShape(UnionShape unionShape) { + Symbol unionType = context.symbolProvider().toSymbol(unionShape); + String shapeName = unionType.getName(); + Collection unionMemberShapes = unionShape.members(); - writer + writer .write("") .openBlock("class $L", symbolProvider.toSymbol(unionShape).getName()) .openBlock("def self.validate!(input, context:)") @@ -197,15 +199,15 @@ public Void unionShape(UnionShape unionShape) { .write("end") // end switch case .closeBlock("end") // end validate method .withQualifiedNamespace("Validators", - () -> renderValidatorsForUnionMembers(unionMemberShapes)) + () -> renderValidatorsForUnionMembers(unionMemberShapes)) .closeBlock("end"); - return null; - } + return null; + } - @Override - public Void documentShape(DocumentShape documentShape) { - writer + @Override + public Void documentShape(DocumentShape documentShape) { + writer .write("") .openBlock("class $L", symbolProvider.toSymbol(documentShape).getName()) .openBlock("def self.validate!(input, context:)") @@ -229,15 +231,15 @@ public Void documentShape(DocumentShape documentShape) { .closeBlock("end") .closeBlock("end"); - return null; - } + return null; + } - private void renderValidatorsForUnionMembers(Collection members) { - members.forEach(member -> { - String name = symbolProvider.toMemberName(member); - Shape target = model.expectShape(member.getTarget()); + private void renderValidatorsForUnionMembers(Collection members) { + members.forEach(member -> { + String name = symbolProvider.toMemberName(member); + Shape target = model.expectShape(member.getTarget()); - writer + writer .write("") .openBlock("class $L", name) .openBlock("def self.validate!(input, context:)") @@ -245,31 +247,7 @@ private void renderValidatorsForUnionMembers(Collection members) { new MemberValidator(writer, symbolProvider, "input", "context", true))) .closeBlock("end") .closeBlock("end"); - }); - } - - @Override - protected Void getDefault(Shape shape) { - return null; - } - - private static class MemberValidator extends ShapeVisitor.Default { - private final RubyCodeWriter writer; - private final SymbolProvider symbolProvider; - private final String input; - private final String context; - private Boolean renderUnionMemberValidator; - - MemberValidator(RubyCodeWriter writer, - SymbolProvider symbolProvider, - String input, - String context, - Boolean renderUnionMemberValidator) { - this.writer = writer; - this.symbolProvider = symbolProvider; - this.input = input; - this.context = context; - this.renderUnionMemberValidator = renderUnionMemberValidator; + }); } @Override @@ -277,136 +255,161 @@ protected Void getDefault(Shape shape) { return null; } - @Override - public Void blobShape(BlobShape shape) { - if (shape.hasTrait(StreamingTrait.class)) { - writer - .openBlock("unless $1L.respond_to?(:read) || $1L.respond_to?(:readpartial)", - input) - .write("raise ArgumentError, \"Expected #{context} to be an IO like object," - + " got #{$L.class}\"", input) - .closeBlock("end"); - if (shape.hasTrait(RequiresLengthTrait.class)) { + private static class MemberValidator extends ShapeVisitor.Default { + private final RubyCodeWriter writer; + private final SymbolProvider symbolProvider; + private final String input; + private final String context; + private Boolean renderUnionMemberValidator; + + MemberValidator(RubyCodeWriter writer, + SymbolProvider symbolProvider, + String input, + String context, + Boolean renderUnionMemberValidator) { + this.writer = writer; + this.symbolProvider = symbolProvider; + this.input = input; + this.context = context; + this.renderUnionMemberValidator = renderUnionMemberValidator; + } + + @Override + protected Void getDefault(Shape shape) { + return null; + } + + @Override + public Void blobShape(BlobShape shape) { + if (shape.hasTrait(StreamingTrait.class)) { writer - .openBlock("\nunless $1L.respond_to?(:size)", input) - .write("raise ArgumentError, \"Expected #{context} to respond_to(:size)\"") + .openBlock("unless $1L.respond_to?(:read) || $1L.respond_to?(:readpartial)", + input) + .write("raise ArgumentError, \"Expected #{context} to be an IO like object," + + " got #{$L.class}\"", input) .closeBlock("end"); + if (shape.hasTrait(RequiresLengthTrait.class)) { + writer + .openBlock("\nunless $1L.respond_to?(:size)", input) + .write("raise ArgumentError, \"Expected #{context} to respond_to(:size)\"") + .closeBlock("end"); + } + } else { + writer.write("$T.validate_types!($L, ::String, context: $L)", Hearth.VALIDATOR, input, context); } - } else { - writer.write("$T.validate_types!($L, ::String, context: $L)", Hearth.VALIDATOR, input, context); + return null; } - return null; - } - @Override - public Void booleanShape(BooleanShape shape) { - writer.write("$T.validate_types!($L, ::TrueClass, ::FalseClass, context: $L)", - Hearth.VALIDATOR, input, context); - return null; - } + @Override + public Void booleanShape(BooleanShape shape) { + writer.write("$T.validate_types!($L, ::TrueClass, ::FalseClass, context: $L)", + Hearth.VALIDATOR, input, context); + return null; + } - @Override - public Void listShape(ListShape shape) { - String content = "$1L.validate!($2L, context: $3L) unless $2L.nil?"; - if (renderUnionMemberValidator) { - content = "Validators::" + content; + @Override + public Void listShape(ListShape shape) { + String content = "$1L.validate!($2L, context: $3L) unless $2L.nil?"; + if (renderUnionMemberValidator) { + content = "Validators::" + content; + } + writer.write(content, symbolProvider.toSymbol(shape).getName(), input, context); + return null; } - writer.write(content, symbolProvider.toSymbol(shape).getName(), input, context); - return null; - } - @Override - public Void byteShape(ByteShape shape) { - writer.write("$T.validate_types!($L, ::Integer, context: $L)", Hearth.VALIDATOR, input, context); - return null; - } + @Override + public Void byteShape(ByteShape shape) { + writer.write("$T.validate_types!($L, ::Integer, context: $L)", Hearth.VALIDATOR, input, context); + return null; + } - @Override - public Void shortShape(ShortShape shape) { - writer.write("$T.validate_types!($L, ::Integer, context: $L)", Hearth.VALIDATOR, input, context); - return null; - } + @Override + public Void shortShape(ShortShape shape) { + writer.write("$T.validate_types!($L, ::Integer, context: $L)", Hearth.VALIDATOR, input, context); + return null; + } - @Override - public Void integerShape(IntegerShape shape) { - writer.write("$T.validate_types!($L, ::Integer, context: $L)", Hearth.VALIDATOR, input, context); - return null; - } + @Override + public Void integerShape(IntegerShape shape) { + writer.write("$T.validate_types!($L, ::Integer, context: $L)", Hearth.VALIDATOR, input, context); + return null; + } - @Override - public Void longShape(LongShape shape) { - writer.write("$T.validate_types!($L, ::Integer, context: $L)", Hearth.VALIDATOR, input, context); - return null; - } + @Override + public Void longShape(LongShape shape) { + writer.write("$T.validate_types!($L, ::Integer, context: $L)", Hearth.VALIDATOR, input, context); + return null; + } - @Override - public Void floatShape(FloatShape shape) { - writer.write("$T.validate_types!($L, ::Float, context: $L)", Hearth.VALIDATOR, input, context); - return null; - } + @Override + public Void floatShape(FloatShape shape) { + writer.write("$T.validate_types!($L, ::Float, context: $L)", Hearth.VALIDATOR, input, context); + return null; + } - @Override - public Void documentShape(DocumentShape shape) { - String content = "$1L.validate!($2L, context: $3L) unless $2L.nil?"; - if (renderUnionMemberValidator) { - content = "Validators::" + content; + @Override + public Void documentShape(DocumentShape shape) { + String content = "$1L.validate!($2L, context: $3L) unless $2L.nil?"; + if (renderUnionMemberValidator) { + content = "Validators::" + content; + } + writer.write(content, symbolProvider.toSymbol(shape).getName(), input, context); + return null; } - writer.write(content, symbolProvider.toSymbol(shape).getName(), input, context); - return null; - } - @Override - public Void doubleShape(DoubleShape shape) { - writer.write("$T.validate_types!($L, ::Float, context: $L)", Hearth.VALIDATOR, input, context); - return null; - } + @Override + public Void doubleShape(DoubleShape shape) { + writer.write("$T.validate_types!($L, ::Float, context: $L)", Hearth.VALIDATOR, input, context); + return null; + } - @Override - public Void bigDecimalShape(BigDecimalShape shape) { - writer.write("$T.validate_types!($L, $T, context: $L)", + @Override + public Void bigDecimalShape(BigDecimalShape shape) { + writer.write("$T.validate_types!($L, $T, context: $L)", Hearth.VALIDATOR, input, RubyImportContainer.BIG_DECIMAL, context); - return null; - } - - @Override - public Void mapShape(MapShape shape) { - String content = "$1L.validate!($2L, context: $3L) unless $2L.nil?"; - if (renderUnionMemberValidator) { - content = "Validators::" + content; + return null; } - writer.write(content, symbolProvider.toSymbol(shape).getName(), input, context); - return null; - } + @Override + public Void mapShape(MapShape shape) { + String content = "$1L.validate!($2L, context: $3L) unless $2L.nil?"; + if (renderUnionMemberValidator) { + content = "Validators::" + content; + } - @Override - public Void stringShape(StringShape shape) { - writer.write("$T.validate_types!($L, ::String, context: $L)", Hearth.VALIDATOR, input, context); - return null; - } + writer.write(content, symbolProvider.toSymbol(shape).getName(), input, context); + return null; + } - @Override - public Void structureShape(StructureShape shape) { - String content = "$1L.validate!($2L, context: $3L) unless $2L.nil?"; - if (renderUnionMemberValidator) { - content = "Validators::" + content; + @Override + public Void stringShape(StringShape shape) { + writer.write("$T.validate_types!($L, ::String, context: $L)", Hearth.VALIDATOR, input, context); + return null; } - writer.write(content, symbolProvider.toSymbol(shape).getName(), input, context); - return null; - } - @Override - public Void unionShape(UnionShape shape) { - writer.write("$1L.validate!($2L, context: $3L) unless $2L.nil?", + @Override + public Void structureShape(StructureShape shape) { + String content = "$1L.validate!($2L, context: $3L) unless $2L.nil?"; + if (renderUnionMemberValidator) { + content = "Validators::" + content; + } + writer.write(content, symbolProvider.toSymbol(shape).getName(), input, context); + return null; + } + + @Override + public Void unionShape(UnionShape shape) { + writer.write("$1L.validate!($2L, context: $3L) unless $2L.nil?", symbolProvider.toSymbol(shape).getName(), input, context); - return null; - } + return null; + } - @Override - public Void timestampShape(TimestampShape shape) { - writer.write("$T.validate_types!($L, $T, context: $L)", + @Override + public Void timestampShape(TimestampShape shape) { + writer.write("$T.validate_types!($L, $T, context: $L)", Hearth.VALIDATOR, input, RubyImportContainer.TIME, context); - return null; + return null; + } } } } diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/WaitersGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/WaitersGenerator.java index 955db9770..2d8216995 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/WaitersGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/WaitersGenerator.java @@ -19,10 +19,10 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.logging.Logger; import java.util.stream.Collectors; -import software.amazon.smithy.build.FileManifest; -import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.codegen.core.directed.GenerateServiceDirective; import software.amazon.smithy.jmespath.ExpressionSerializer; import software.amazon.smithy.jmespath.ExpressionVisitor; import software.amazon.smithy.jmespath.JmespathExpression; @@ -44,8 +44,6 @@ import software.amazon.smithy.jmespath.ast.ProjectionExpression; import software.amazon.smithy.jmespath.ast.SliceExpression; import software.amazon.smithy.jmespath.ast.Subexpression; -import software.amazon.smithy.model.Model; -import software.amazon.smithy.model.knowledge.TopDownIndex; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.ruby.codegen.GenerationContext; import software.amazon.smithy.ruby.codegen.Hearth; @@ -61,68 +59,54 @@ import software.amazon.smithy.waiters.Waiter; @SmithyInternalApi -public class WaitersGenerator { +public class WaitersGenerator extends RubyGeneratorBase { private static final Logger LOGGER = Logger.getLogger(WaitersGenerator.class.getName()); - private final GenerationContext context; - private final RubySettings settings; - private final Model model; - private final RubyCodeWriter writer; - private final RubyCodeWriter rbsWriter; - private final SymbolProvider symbolProvider; - - public WaitersGenerator(GenerationContext context) { - this.context = context; - this.settings = context.settings(); - this.model = context.model(); - this.writer = new RubyCodeWriter(context.settings().getModule() + "::Waiters"); - this.rbsWriter = new RubyCodeWriter(context.settings().getModule() + "::Waiters"); - this.symbolProvider = context.symbolProvider(); + private final Set operations; + + public WaitersGenerator(GenerateServiceDirective directive) { + super(directive); + this.operations = directive.operations(); } - public void render() { - FileManifest fileManifest = context.fileManifest(); + @Override + String getModule() { + return "Waiters"; + } - writer + public void render() { + write(writer -> { + writer .includePreamble() .includeRequires() .openBlock("module $L", settings.getModule()) .openBlock("module Waiters") - .call(() -> renderWaiters(false)) + .call(() -> renderWaiters(writer, false)) .write("") .closeBlock("end") .closeBlock("end"); - - String fileName = settings.getGemName() + "/lib/" + settings.getGemName() + "/waiters.rb"; - fileManifest.writeFile(fileName, writer.toString()); - LOGGER.fine("Wrote waiters to " + fileName); + }); + LOGGER.fine("Wrote waiters to " + rbFile()); } public void renderRbs() { - FileManifest fileManifest = context.fileManifest(); - - rbsWriter + writeRbs(writer -> { + writer .includePreamble() .openBlock("module $L", settings.getModule()) .openBlock("module Waiters") - .call(() -> renderWaiters(true)) + .call(() -> renderWaiters(writer, true)) .write("") .closeBlock("end") .closeBlock("end"); - - String typesFile = - settings.getGemName() + "/sig/" + settings.getGemName() - + "/waiters.rbs"; - fileManifest.writeFile(typesFile, rbsWriter.toString()); - LOGGER.fine("Wrote waiters rbs to " + typesFile); + }); + LOGGER.fine("Wrote waiters rbs to " + rbsFile()); } - private void renderWaiters(Boolean rbs) { - TopDownIndex topDownIndex = TopDownIndex.of(model); - - topDownIndex.getContainedOperations(context.service()).stream().forEach((operation) -> { + private void renderWaiters(RubyCodeWriter writer, Boolean rbs) { + operations.forEach((operation) -> { if (operation.hasTrait(WaitableTrait.class)) { Map waiters = operation.getTrait(WaitableTrait.class).get().getWaiters(); Iterator> iterator = waiters.entrySet().iterator(); @@ -132,9 +116,9 @@ private void renderWaiters(Boolean rbs) { String waiterName = entry.getKey(); Waiter waiter = entry.getValue(); if (rbs) { - renderRbsWaiter(waiterName); + renderRbsWaiter(writer, waiterName); } else { - renderWaiter(waiterName, waiter, operation); + renderWaiter(writer, waiterName, waiter, operation); } if (iterator.hasNext()) { writer.write(""); @@ -144,14 +128,14 @@ private void renderWaiters(Boolean rbs) { }); } - private void renderWaiter(String waiterName, Waiter waiter, OperationShape operation) { + private void renderWaiter(RubyCodeWriter writer, String waiterName, Waiter waiter, OperationShape operation) { String operationName = RubyFormatter.toSnakeCase(symbolProvider.toSymbol(operation).getName()); writer .write("") - .call(() -> renderWaiterDocumentation(waiter)) + .call(() -> renderWaiterDocumentation(writer, waiter)) .openBlock("class $L", waiterName) - .call(() -> renderWaiterInitializeDocumentation(waiter)) + .call(() -> renderWaiterInitializeDocumentation(writer, waiter)) .openBlock("def initialize(client, options = {})") .write("@client = client") .openBlock("@waiter = $T.new({", Hearth.WAITER) @@ -160,15 +144,15 @@ private void renderWaiter(String waiterName, Waiter waiter, OperationShape opera .write("max_delay: $L || options[:max_delay],", waiter.getMaxDelay()) .openBlock("poller: $T.new(", Hearth.POLLER) .write("operation_name: :$L,", operationName) - .call(() -> renderAcceptors(waiter)) + .call(() -> renderAcceptors(writer, waiter)) .closeBlock(")") .closeBlock("}.merge(options))") - .call(() -> renderWaiterTags(waiter)) + .call(() -> renderWaiterTags(writer, waiter)) .closeBlock("end") .write("") .write("attr_reader :tags") .write("") - .call(() -> renderWaiterWaitDocumentation(operation, operationName)) + .call(() -> renderWaiterWaitDocumentation(writer, operation, operationName)) .openBlock("def wait(params = {}, options = {})") .write("@waiter.wait(@client, params, options)") .closeBlock("end") @@ -177,8 +161,8 @@ private void renderWaiter(String waiterName, Waiter waiter, OperationShape opera LOGGER.finer("Generated waiter " + waiterName + " for operation: " + operationName); } - private void renderRbsWaiter(String waiterName) { - rbsWriter + private void renderRbsWaiter(RubyCodeWriter writer, String waiterName) { + writer .write("") .openBlock("class $L", waiterName) .write("def initialize: (untyped client, ?::Hash[untyped, untyped] options) -> void\n") @@ -187,7 +171,7 @@ private void renderRbsWaiter(String waiterName) { .closeBlock("end"); } - private void renderWaiterDocumentation(Waiter waiter) { + private void renderWaiterDocumentation(RubyCodeWriter writer, Waiter waiter) { if (waiter.getDocumentation().isPresent()) { writer.writeDocstring(waiter.getDocumentation().get()); } @@ -196,14 +180,14 @@ private void renderWaiterDocumentation(Waiter waiter) { } } - private void renderWaiterTags(Waiter waiter) { + private void renderWaiterTags(RubyCodeWriter writer, Waiter waiter) { String tags = waiter.getTags().stream() .map((tag) -> "\"" + tag + "\"") .collect(Collectors.joining(", ")); writer.write("@tags = [$L]", tags); } - private void renderAcceptors(Waiter waiter) { + private void renderAcceptors(RubyCodeWriter writer, Waiter waiter) { List acceptorsList = waiter.getAcceptors(); if (acceptorsList.isEmpty()) { @@ -221,7 +205,7 @@ private void renderAcceptors(Waiter waiter) { .write("state: '$L',", state) .openBlock("matcher: {") .call(() -> { - matcher.accept(new AcceptorVisitor()); + matcher.accept(new AcceptorVisitor(writer)); }) .closeBlock("}"); @@ -241,7 +225,7 @@ private String translatePath(String path) { return transformedPath; } - private void renderWaiterWaitDocumentation(OperationShape operation, String operationName) { + private void renderWaiterWaitDocumentation(RubyCodeWriter writer, OperationShape operation, String operationName) { String operationReturnType = "Types::" + symbolProvider.toSymbol(operation).getName(); String operationReference = "(see Client#" + operationName + ")"; @@ -251,7 +235,7 @@ private void renderWaiterWaitDocumentation(OperationShape operation, String oper .writeYardReturn(operationReturnType, operationReference); } - private void renderWaiterInitializeDocumentation(Waiter waiter) { + private void renderWaiterInitializeDocumentation(RubyCodeWriter writer, Waiter waiter) { writer .writeYardParam("Client", "client", "") .writeYardParam("Hash", "options", "") @@ -275,7 +259,13 @@ private void renderWaiterInitializeDocumentation(Waiter waiter) { "The maximum time in seconds to delay polling attempts."); } - private class AcceptorVisitor implements Matcher.Visitor { + private final class AcceptorVisitor implements Matcher.Visitor { + + private final RubyCodeWriter writer; + + private AcceptorVisitor(RubyCodeWriter writer) { + this.writer = writer; + } private void renderPathMatcher(String memberName, String path, String comparator, String expected) { writer diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/docs/TraitExampleGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/docs/TraitExampleGenerator.java index 961e32e70..48dbea1ba 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/docs/TraitExampleGenerator.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/docs/TraitExampleGenerator.java @@ -18,6 +18,7 @@ import java.util.Optional; import software.amazon.smithy.codegen.core.SymbolProvider; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.Shape; @@ -36,7 +37,7 @@ public class TraitExampleGenerator { private final RubyCodeWriter writer; private final Optional documentation; private final ObjectNode input; - private final ObjectNode output; + private final Node output; private final Optional error; private final String operationName; private final Shape operationInput; @@ -50,7 +51,11 @@ public TraitExampleGenerator(OperationShape operation, SymbolProvider symbolProv this.writer = new RubyCodeWriter(""); this.documentation = example.getDocumentation(); this.input = example.getInput(); - this.output = example.getOutput(); + if (example.getOutput().isPresent()) { + this.output = example.getOutput().get(); + } else { + this.output = ObjectNode.nullNode(); + } this.error = example.getError(); this.operationName = diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/Middleware.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/Middleware.java index 1de3c7b06..39e2c5941 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/Middleware.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/Middleware.java @@ -28,6 +28,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import software.amazon.smithy.codegen.core.CodegenException; @@ -54,6 +55,8 @@ public final class Middleware { private final String klass; private final MiddlewareStackStep step; private final byte order; + + private final Optional relative; private final Set clientConfig; private final OperationParams operationParams; private final Map additionalParams; @@ -68,6 +71,7 @@ private Middleware(Builder builder) { this.klass = builder.klass; this.step = builder.step; this.order = builder.order; + this.relative = builder.relative; this.clientConfig = builder.clientConfig; this.operationParams = builder.operationParams; this.additionalParams = builder.additionalParams; @@ -98,6 +102,13 @@ public byte getOrder() { return order; } + /** + * @return relative order within stack step + */ + public Optional getRelative() { + return relative; + } + /** * @return clientConfig to be added to the client to support this middleware. */ @@ -210,7 +221,7 @@ public static class Builder implements SmithyBuilder { Set config = middleware.getClientConfig(); Map params = - middleware.getAdditionalParams(); + new HashMap<>(middleware.getAdditionalParams()); params.putAll(middleware.operationParams .params(context, operation)); @@ -236,6 +247,7 @@ public static class Builder implements SmithyBuilder { } }; private byte order = 0; + private Optional relative = Optional.empty(); private String klass; private MiddlewareStackStep step; private Set clientConfig = new HashSet<>(); @@ -263,10 +275,21 @@ public Builder klass(String klass) { * @return Returns the builder */ public Builder order(byte order) { + if (relative.isPresent()) { + throw new IllegalArgumentException("Cannot combine relative ordering with explicit order value."); + } this.order = order; return this; } + public Builder relative(Relative relative) { + if (order != 0) { + throw new IllegalArgumentException("Cannot combine relative ordering with explicit order value."); + } + this.relative = Optional.of(relative); + return this; + } + /** * @param step The step to apply the middleware to. * @return Returns the builder @@ -467,4 +490,31 @@ public Middleware build() { return new Middleware(this); } } + + public static class Relative { + private final Type type; + private final String to; + + public Relative(Type type, String to) { + this.type = type; + this.to = to; + } + + public Type getType() { + return type; + } + + public String getTo() { + return to; + } + + public String toString() { + return type + " " + to; + } + + public enum Type { + BEFORE, + AFTER + } + } } diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/MiddlewareBuilder.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/MiddlewareBuilder.java index 8aac09903..caf721934 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/MiddlewareBuilder.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/middleware/MiddlewareBuilder.java @@ -23,6 +23,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import software.amazon.smithy.codegen.core.SymbolProvider; @@ -38,6 +39,7 @@ import software.amazon.smithy.ruby.codegen.RubyCodeWriter; import software.amazon.smithy.ruby.codegen.RubySymbolProvider; import software.amazon.smithy.ruby.codegen.config.ClientConfig; +import software.amazon.smithy.ruby.codegen.config.ConfigProviderChain; import software.amazon.smithy.utils.SmithyInternalApi; @SmithyInternalApi @@ -81,6 +83,9 @@ public Set getClientConfig(GenerationContext context) { .collect(Collectors.toSet()); config.addAll(stepConfig); } + + config.addAll(getDefaultClientConfig()); + return config; } @@ -100,11 +105,7 @@ public void render(RubyCodeWriter writer, GenerationContext context, ServiceShape service = context.service(); for (MiddlewareStackStep step : MiddlewareStackStep.values()) { - List orderedStepMiddleware = middlewares.get(step) - .stream() - .filter((m) -> m.includeFor(model, service, operation)) - .sorted(Comparator.comparing(Middleware::getOrder)) - .collect(Collectors.toList()); + List orderedStepMiddleware = resolveAndFilter(step, model, service, operation); for (Middleware middleware : orderedStepMiddleware) { middleware.renderAdd(writer, context, operation); @@ -112,6 +113,61 @@ public void render(RubyCodeWriter writer, GenerationContext context, } } + private List resolveAndFilter(MiddlewareStackStep step, Model model, ServiceShape service, + OperationShape operation) { + Set resolved = new HashSet<>(); + Set visiting = new HashSet<>(); + Map order = new HashMap<>(); + Map klassToMiddlewareMap = new HashMap<>(); + + + List filteredMiddleware = middlewares.get(step) + .stream().filter((m) -> m.includeFor(model, service, operation)) + .collect(Collectors.toList()); + + filteredMiddleware.forEach((m) -> klassToMiddlewareMap.put(m.getKlass(), m)); + + for (Middleware middleware : filteredMiddleware) { + resolve(middleware, resolved, visiting, order, klassToMiddlewareMap); + } + + return filteredMiddleware.stream() + .sorted(Comparator.comparingInt(order::get)) + .collect(Collectors.toList()); + } + + private void resolve(Middleware middleware, Set resolved, Set visiting, + Map order, Map klassToMiddlewareMap) { + + if (visiting.contains(middleware)) { + throw new IllegalArgumentException("Circular dependency detected when resolving order for middleware: " + + middleware.getKlass()); + } + // skip if its already been resolved + if (!resolved.contains(middleware)) { + visiting.add(middleware); + if (middleware.getRelative().isPresent()) { + Middleware relativeTo = Objects.requireNonNull( + klassToMiddlewareMap.get(middleware.getRelative().get().getTo()), + middleware.getKlass() + " relative references a middleware class (" + + middleware.getRelative().get().getTo() + ") that is not available in the stack."); + //recursively resolve the relative middleware + resolve(relativeTo, resolved, visiting, order, klassToMiddlewareMap); + // the order of relativeTo should now be set + if (middleware.getRelative().get().getType().equals(Middleware.Relative.Type.BEFORE)) { + order.put(middleware, order.get(relativeTo) - 1); + } else { + order.put(middleware, order.get(relativeTo) + 1); + } + } else { + // Base case - middleware is not relative to anything else, use its default order. + order.put(middleware, (int) middleware.getOrder()); + } + visiting.remove(middleware); + resolved.add(middleware); + } + } + public void addDefaultMiddleware(GenerationContext context) { ApplicationTransport transport = context.applicationTransport(); SymbolProvider symbolProvider = context.symbolProvider(); @@ -178,55 +234,30 @@ public void addDefaultMiddleware(GenerationContext context) { "Enable response stubbing for testing. See {Hearth::ClientStubs#stub_responses}.") .build(); - ClientConfig maxAttempts = (new ClientConfig.Builder()) - .name("max_attempts") - .type("Integer") - .defaultValue("3") + ClientConfig retryStrategy = (new ClientConfig.Builder()) + .name("retry_strategy") + .type("Hearth::Retry::Strategy") + .documentationDefaultValue("Hearth::Retry::Standard.new") + .defaultValue("Hearth::Retry::Standard.new") .documentation( - "An integer representing the maximum number of attempts that will be made for a " - + "single request, including the initial attempt." - ) - .build(); - - ClientConfig retryMode = (new ClientConfig.Builder()) - .name("retry_mode") - .type("String") - .defaultValue("'standard'") - .documentation( - "Specifies which retry algorithm to use. Values are: \n" - + - " * `standard` - A standardized set of retry rules across the AWS SDKs. " - + - "This includes support" - + - " for retry quotas, which limit the number of unsuccessful retries a client can make.\n" - + " * `adaptive` - An experimental retry mode that includes all the" + "Specifies which retry strategy class to use. Strategy classes\n" + + " may have additional options, such as max_retries and backoff strategies.\n" + + " Available options are: \n" + + " * `Retry::Standard` - A standardized set of retry rules across the AWS SDKs. " + + "This includes support for retry quotas, which limit the number of" + + " unsuccessful retries a client can make.\n" + + " * `Retry::Adaptive` - An experimental retry mode that includes all the" + " functionality of `standard` mode along with automatic client side" + " throttling. This is a provisional mode that may change behavior" + " in the future." ) .build(); - ClientConfig adaptiveRetryWaitToFill = (new ClientConfig.Builder()) - .name("adaptive_retry_wait_to_fill") - .type("Boolean") - .defaultValue("true") - .documentation( - "Used only in `adaptive` retry mode. When true, the request will sleep until there is" - + " sufficient client side capacity to retry the request. When false, the request will" - + " raise a `CapacityNotAvailableError` and will not retry instead of sleeping." - ) - .build(); - Middleware retry = (new Middleware.Builder()) .klass("Hearth::Middleware::Retry") .step(MiddlewareStackStep.RETRY) - .addConfig(maxAttempts) - .addConfig(retryMode) - .addConfig(adaptiveRetryWaitToFill) - .addParam("error_inspector_class", "Hearth::Retry::ErrorInspector") - .addParam("retry_quota", "@retry_quota") - .addParam("client_rate_limiter", "@client_rate_limiter") + .addConfig(retryStrategy) + .addParam("error_inspector_class", transport.getErrorInspector()) .build(); Middleware send = (new Middleware.Builder()) @@ -252,4 +283,26 @@ public void addDefaultMiddleware(GenerationContext context) { register(transport.defaultMiddleware(context)); } + + private Collection getDefaultClientConfig() { + ClientConfig logger = (new ClientConfig.Builder()) + .name("logger") + .type("Logger") + .documentationDefaultValue("Logger.new($stdout, level: cfg.log_level)") + .defaults(new ConfigProviderChain.Builder() + .dynamicProvider("proc { |cfg| Logger.new($stdout, level: cfg[:log_level]) } ") + .build() + ) + .documentation("The Logger instance to use for logging.") + .build(); + + ClientConfig logLevel = (new ClientConfig.Builder()) + .name("log_level") + .type("Symbol") + .defaultValue(":info") + .documentation("The default log level to use with the Logger.") + .build(); + + return Arrays.asList(logger, logLevel); + } } diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/util/ParamsToHash.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/util/ParamsToHash.java index d0dcbc5a5..a0a8a9036 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/util/ParamsToHash.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/util/ParamsToHash.java @@ -192,7 +192,15 @@ public String timestampShape(TimestampShape shape) { return ""; } if (node.isNumberNode()) { - return "Time.at(" + node.expectNumberNode().getValue().toString() + ")"; + if (node.expectNumberNode().isFloatingPointNumber()) { + // rounding of floats cause an issue in the precision of fractional seconds + Double n = node.expectNumberNode().getValue().doubleValue(); + long seconds = (long) Math.floor(n); + long ms = Math.round((n - Math.floor(n)) * 1000); + return "Time.at(" + seconds + ", " + ms + ", :millisecond)"; + } else { + return "Time.at(" + node.expectNumberNode().getValue().toString() + ")"; + } } return "Time.parse('" + node + "')"; } diff --git a/codegen/smithy-ruby-rails-codegen-test/integration-specs/request_id_spec.rb b/codegen/smithy-ruby-rails-codegen-test/integration-specs/request_id_spec.rb index 257d89c85..350607ef5 100644 --- a/codegen/smithy-ruby-rails-codegen-test/integration-specs/request_id_spec.rb +++ b/codegen/smithy-ruby-rails-codegen-test/integration-specs/request_id_spec.rb @@ -13,7 +13,7 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 200 - response.headers = Hearth::HTTP::Headers.new({ 'x-request-id' => '123' }) + response.headers['x-request-id'] = '123' response.body = StringIO.new('{}') Hearth::Output.new end @@ -28,9 +28,8 @@ module RailsJson middleware = Hearth::MiddlewareBuilder.around_send do |app, input, context| response = context.response response.status = 400 - response.headers = Hearth::HTTP::Headers.new( - { 'x-smithy-rails-error' => 'InvalidGreeting', 'x-request-id' => '123' } - ) + response.headers['x-smithy-rails-error'] = 'InvalidGreeting' + response.headers['x-request-id'] = '123' response.body = StringIO.new('{}') Hearth::Output.new end diff --git a/codegen/smithy-ruby-rails-codegen-test/model/high-score-service.smithy b/codegen/smithy-ruby-rails-codegen-test/model/high-score-service.smithy index 147fc73b4..2cfb28813 100644 --- a/codegen/smithy-ruby-rails-codegen-test/model/high-score-service.smithy +++ b/codegen/smithy-ruby-rails-codegen-test/model/high-score-service.smithy @@ -10,7 +10,7 @@ use smithy.ruby.protocols#UnprocessableEntityError @title("High Score Sample Rails Service") service HighScoreService { version: "2021-02-15", - resources: [HighScore], + resources: [HighScore] } /// Rails default scaffold operations diff --git a/codegen/smithy-ruby-rails-codegen-test/model/protocol-test/kitchen-sink.smithy b/codegen/smithy-ruby-rails-codegen-test/model/protocol-test/kitchen-sink.smithy index 683c9a535..9c4d9023b 100644 --- a/codegen/smithy-ruby-rails-codegen-test/model/protocol-test/kitchen-sink.smithy +++ b/codegen/smithy-ruby-rails-codegen-test/model/protocol-test/kitchen-sink.smithy @@ -168,6 +168,22 @@ use smithy.test#httpResponseTests method: "POST", uri: "/", }, + { + id: "rails_json_serializes_fractional_timestamp_shapes", + protocol: railsJson, + documentation: "Serializes fractional timestamp shapes", + body: "{\"timestamp\":\"2000-01-02T20:34:56.123Z\"}", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + requireHeaders: [ + "Content-Length" + ], + params: { + Timestamp: 946845296.123, + }, + method: "POST", + uri: "/", + }, { id: "rails_json_serializes_timestamp_shapes_with_iso8601_timestampformat", protocol: railsJson, @@ -664,6 +680,18 @@ use smithy.test#httpResponseTests }, code: 200, }, + { + id: "rails_json_parses_fractional_timestamp_shapes", + protocol: railsJson, + documentation: "Parses fractional timestamp shapes", + body: "{\"timestamp\":\"2000-01-02T20:34:56.123Z\"}", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: { + Timestamp: 946845296.123, + }, + code: 200, + }, { id: "rails_json_parses_iso8601_timestamps", protocol: railsJson, @@ -688,6 +716,18 @@ use smithy.test#httpResponseTests }, code: 200, }, + { + id: "rails_json_parses_fractional_httpdate_timestamps", + protocol: railsJson, + documentation: "Parses fractional httpdate timestamps", + body: "{\"httpdate_timestamp\":\"Sun, 02 Jan 2000 20:34:56.123 GMT\"}", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: { + HttpdateTimestamp: 946845296.123, + }, + code: 200, + }, { id: "rails_json_parses_list_shapes", protocol: railsJson, diff --git a/codegen/smithy-ruby-rails-codegen-test/model/protocol-test/main.smithy b/codegen/smithy-ruby-rails-codegen-test/model/protocol-test/main.smithy index 3b42e6730..94b004ccc 100644 --- a/codegen/smithy-ruby-rails-codegen-test/model/protocol-test/main.smithy +++ b/codegen/smithy-ruby-rails-codegen-test/model/protocol-test/main.smithy @@ -11,6 +11,10 @@ use smithy.test#httpResponseTests @title("RailsJson Protocol Test Service") service RailsJson { version: "2018-01-01", + // Ensure that generators are able to handle renames. + rename: { + "aws.protocoltests.restjson.nested#GreetingStruct": "RenamedGreeting", + }, operations: [ KitchenSinkOperation, EndpointOperation, diff --git a/codegen/smithy-ruby-rails-codegen-test/model/protocol-test/unions.smithy b/codegen/smithy-ruby-rails-codegen-test/model/protocol-test/unions.smithy index 35dc52376..5630b2493 100644 --- a/codegen/smithy-ruby-rails-codegen-test/model/protocol-test/unions.smithy +++ b/codegen/smithy-ruby-rails-codegen-test/model/protocol-test/unions.smithy @@ -37,6 +37,10 @@ union MyUnion { listValue: StringList, mapValue: StringMap, structureValue: GreetingStruct, + + // Note that this uses a conflicting structure name with + // GreetingStruct, so it must be renamed in the service. + renamedStructureValue: aws.protocoltests.restjson.nested#GreetingStruct, } apply JsonUnions @httpRequestTests([ @@ -248,6 +252,30 @@ apply JsonUnions @httpRequestTests([ } } }, + { + id: "RailsJsonSerializeRenamedStructureUnionValue", + documentation: "Serializes a renamed structure union value", + protocol: railsJson, + method: "POST", + uri: "/jsonunions", + body: """ + { + "contents": { + "renamed_structure_value": { + "salutation": "hello!" + } + } + }""", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: { + contents: { + renamedStructureValue: { + salutation: "hello!", + } + } + } + }, ]) apply JsonUnions @httpResponseTests([ diff --git a/codegen/smithy-ruby-rails-codegen/src/main/java/software/amazon/smithy/ruby/codegen/protocol/railsjson/generators/StubsGenerator.java b/codegen/smithy-ruby-rails-codegen/src/main/java/software/amazon/smithy/ruby/codegen/protocol/railsjson/generators/StubsGenerator.java index 16606d896..c97ef14f3 100644 --- a/codegen/smithy-ruby-rails-codegen/src/main/java/software/amazon/smithy/ruby/codegen/protocol/railsjson/generators/StubsGenerator.java +++ b/codegen/smithy-ruby-rails-codegen/src/main/java/software/amazon/smithy/ruby/codegen/protocol/railsjson/generators/StubsGenerator.java @@ -115,7 +115,7 @@ protected void renderBodyStub(OperationShape operation, Shape outputShape) { writer .write("http_resp.headers['Content-Type'] = 'application/json'") .call(() -> renderMemberStubbers(outputShape)) - .write("http_resp.body = StringIO.new(Hearth::JSON.dump(data))"); + .write("http_resp.body.write(Hearth::JSON.dump(data))"); } @Override @@ -300,7 +300,7 @@ protected Void getDefault(Shape shape) { public Void stringShape(StringShape shape) { writer .write("http_resp.headers['Content-Type'] = 'text/plain'") - .write("http_resp.body = StringIO.new($L || '')", inputGetter); + .write("http_resp.body.write($L || '')", inputGetter); return null; } @@ -314,7 +314,7 @@ public Void blobShape(BlobShape shape) { writer .write("http_resp.headers['Content-Type'] = '$L'", mediaType) - .write("http_resp.body = StringIO.new($L || '')", inputGetter); + .write("http_resp.body.write($L || '')", inputGetter); return null; } @@ -323,7 +323,7 @@ public Void blobShape(BlobShape shape) { public Void documentShape(DocumentShape shape) { writer .write("http_resp.headers['Content-Type'] = 'application/json'") - .write("http_resp.body = StringIO.new(Hearth::JSON.dump($1L))", inputGetter); + .write("http_resp.body.write(Hearth::JSON.dump($1L))", inputGetter); return null; } @@ -356,7 +356,7 @@ private void defaultComplexSerializer(Shape shape) { .write("http_resp.headers['Content-Type'] = 'application/json'") .write("data = Stubs::$1L.stub($2L) unless $2L.nil?", symbolProvider.toSymbol(shape).getName(), inputGetter) - .write("http_resp.body = StringIO.new(Hearth::JSON.dump(data))"); + .write("http_resp.body.write(Hearth::JSON.dump(data))"); } } diff --git a/hearth/.rubocop.yml b/hearth/.rubocop.yml index 455add302..d7819a24a 100644 --- a/hearth/.rubocop.yml +++ b/hearth/.rubocop.yml @@ -6,7 +6,11 @@ Metrics: Exclude: - 'spec/**/*.rb' -# For some reason, Metrics disable doesn't cover this +Metrics/AbcSize: + Exclude: + - 'spec/**/*.rb' + - 'lib/hearth/http/client.rb' + Metrics/BlockLength: Exclude: - 'spec/**/*.rb' @@ -19,15 +23,18 @@ Layout/LineLength: Max: 80 Metrics/MethodLength: - Max: 15 + Max: 20 + Exclude: + - 'spec/**/*.rb' + - 'lib/hearth/middleware/send.rb' Metrics/ClassLength: Exclude: + - 'lib/hearth/http/client.rb' - 'lib/hearth/middleware_builder.rb' Metrics/ParameterLists: Exclude: - - 'lib/hearth/middleware/retry.rb' - 'lib/hearth/middleware/send.rb' Style/Documentation: diff --git a/hearth/Gemfile b/hearth/Gemfile index ec18820e3..b8863b4b0 100644 --- a/hearth/Gemfile +++ b/hearth/Gemfile @@ -13,7 +13,8 @@ group :test do end group :development do - gem 'rbs' + gem 'parallel', '1.22.1' # 1.23.0 broke steep, temporary + gem 'rbs', '~> 2' gem 'rubocop' - gem 'steep' + gem 'steep', '1.3.2' end diff --git a/hearth/lib/hearth.rb b/hearth/lib/hearth.rb index c219fcd56..28e2f0b18 100755 --- a/hearth/lib/hearth.rb +++ b/hearth/lib/hearth.rb @@ -6,7 +6,15 @@ require_relative 'hearth/configuration' require_relative 'hearth/config/env_provider' require_relative 'hearth/config/resolver' +require_relative 'hearth/connection_pool' require_relative 'hearth/context' +require_relative 'hearth/dns' + +# must be required before http +require_relative 'hearth/networking_error' +require_relative 'hearth/request' +require_relative 'hearth/response' + require_relative 'hearth/http' require_relative 'hearth/json' require_relative 'hearth/middleware' diff --git a/hearth/lib/hearth/connection_pool.rb b/hearth/lib/hearth/connection_pool.rb new file mode 100644 index 000000000..9dc9a0d37 --- /dev/null +++ b/hearth/lib/hearth/connection_pool.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +module Hearth + # @api private + class ConnectionPool + @pools_mutex = Mutex.new + @pools = {} + + class << self + # @return [ConnectionPool] + def for(config = {}) + @pools_mutex.synchronize do + @pools[config] ||= new + end + end + + # @return [Array] Returns a list of the + # constructed connection pools. + def pools + @pools_mutex.synchronize do + @pools.values + end + end + end + + # @api private + def initialize + @pool_mutex = Mutex.new + @pool = {} + end + + # @param [URI::HTTP, URI::HTTPS] endpoint The HTTP(S) endpoint + # to connect to (e.g. 'https://domain.com'). + # @param [Proc] block A block that returns a new connection if + # there are no connections present. + # @return [Connection, nil] + def connection_for(endpoint, &block) + connection = nil + endpoint = remove_path_and_query(endpoint) + # attempt to recycle an already open connection + @pool_mutex.synchronize do + clean + connection = @pool[endpoint].shift if @pool.key?(endpoint) + end + connection || (block.call if block_given?) + end + + # @param [URI::HTTP, URI::HTTPS] endpoint The HTTP(S) endpoint + # @param [Object] connection The connection to check back into the pool. + # @return [nil] + def offer(endpoint, connection) + endpoint = remove_path_and_query(endpoint) + @pool_mutex.synchronize do + @pool[endpoint] = [] unless @pool.key?(endpoint) + @pool[endpoint] << connection + end + end + + # Closes and removes all connections from the pool. + # If empty! is called while there are outstanding requests they may + # get checked back into the pool, leaving the pool in a non-empty + # state. + # @return [nil] + def empty! + @pool_mutex.synchronize do + @pool.each_pair do |_endpoint, connections| + connections.each(&:finish) + end + @pool.clear + end + nil + end + + private + + # Removes stale connections from the pool. This method *must* be called + # @note **Must** be called behind a `@pool_mutex` synchronize block. + def clean + @pool.each_pair do |_endpoint, connections| + connections.delete_if(&:stale?) + end + end + + # Connection pools should be keyed by endpoint and port. + def remove_path_and_query(endpoint) + endpoint.dup.tap do |e| + e.path = '' + e.query = nil + end.to_s + end + end +end diff --git a/hearth/lib/hearth/dns.rb b/hearth/lib/hearth/dns.rb new file mode 100644 index 000000000..1ad367892 --- /dev/null +++ b/hearth/lib/hearth/dns.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'socket' + +require_relative 'dns/host_address' +require_relative 'dns/host_resolver' + +# These patches are based on resolv-replace +# https://github.com/ruby/ruby/blob/master/lib/resolv-replace.rb +# We cannot require resolv-replace because it would change DNS resolution +# globally. When opening an HTTP request, we will set a thread local variable +# to enable custom DNS resolution, and then disable it after the request is +# complete. When the thread local variable is not set, we will use the default +# Ruby DNS resolution, which may be Resolv or the system resolver. + +# Patch IPSocket +class << IPSocket + alias original_hearth_getaddress getaddress + + def getaddress(host) + unless (resolver = Thread.current[:net_http_hearth_dns_resolver]) + return original_hearth_getaddress(host) + end + + ipv6, ipv4 = resolver.resolve_address(nodename: host) + return ipv6.address if ipv6 + + ipv4.address + end +end + +# Patch TCPSocket +class TCPSocket < IPSocket + alias original_hearth_initialize initialize + + # rubocop:disable Lint/MissingSuper + def initialize(host, serv, *rest) + if Thread.current[:net_http_hearth_dns_resolver] + rest[0] = IPSocket.getaddress(rest[0]) if rest[0] + original_hearth_initialize(IPSocket.getaddress(host), serv, *rest) + else + original_hearth_initialize(host, serv, *rest) + end + end + # rubocop:enable Lint/MissingSuper +end diff --git a/hearth/lib/hearth/dns/host_address.rb b/hearth/lib/hearth/dns/host_address.rb new file mode 100644 index 000000000..817fa2871 --- /dev/null +++ b/hearth/lib/hearth/dns/host_address.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Hearth + module DNS + # Address results from a DNS lookup in {HostResolver}. + class HostAddress + def initialize(address_type:, address:, hostname:) + @address_type = address_type + @address = address + @hostname = hostname + end + + # @return [Symbol] + attr_reader :address_type + + # @return [String] + attr_reader :address + + # @return [String] + attr_reader :hostname + end + end +end diff --git a/hearth/lib/hearth/dns/host_resolver.rb b/hearth/lib/hearth/dns/host_resolver.rb new file mode 100644 index 000000000..7429fe2fa --- /dev/null +++ b/hearth/lib/hearth/dns/host_resolver.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +module Hearth + module DNS + # Resolves a host name and service to an IP address. Can be used with + # {Hearth::HTTP::Client} host_resolver option. This implementation uses + # Addrinfo.getaddrinfo to resolve the host name. + # @see https://ruby-doc.org/stdlib-3.0.2/libdoc/socket/rdoc/Addrinfo.html + class HostResolver + # @param [Integer] service (443) + # @param [Integer] family (nil) + # @param [Symbol] socktype (:SOCK_STREAM) + # @param [Integer] protocol (nil) + # @param [Integer] flags (nil) + def initialize(service: 443, family: nil, socktype: :SOCK_STREAM, + protocol: nil, flags: nil) + @service = service + @family = family + @socktype = socktype + @protocol = protocol + @flags = flags + end + + # @return [Integer] + attr_reader :service + + # @return [Integer] + attr_reader :family + + # @return [Symbol] + attr_reader :socktype + + # @return [Integer] + attr_reader :protocol + + # @return [Integer] + attr_reader :flags + + # @param [String] nodename + # @param (see Hearth::DNS::HostResolver#initialize) + def resolve_address(nodename:, **kwargs) + options = kwargs.merge(nodename: nodename) + addrinfo_list = addrinfo(options) + ipv6 = ipv6_addr(addrinfo_list, options) if use_ipv6? + ipv4 = ipv4_addr(addrinfo_list, options) + [ipv6, ipv4] + end + + private + + def addrinfo(options) + Addrinfo.getaddrinfo( + options[:nodename], + options.fetch(:service, @service), + options.fetch(:family, @family), + options.fetch(:socktype, @socktype), + options.fetch(:protocol, @protocol), + options.fetch(:flags, @flags) + ) + end + + def ipv4_addr(addrinfo_list, options) + addr = addrinfo_list.find(&:ipv4?) + return unless addr + + HostAddress.new( + address_type: :A, + address: addr.ip_address, + hostname: options[:nodename] + ) + end + + def ipv6_addr(addrinfo_list, options) + addr = addrinfo_list.find(&:ipv6?) + return unless addr + + HostAddress.new( + address_type: :AAAA, + address: addr.ip_address, + hostname: options[:nodename] + ) + end + + def use_ipv6? + Socket.ip_address_list.any? do |a| + a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? + end + end + end + end +end diff --git a/hearth/lib/hearth/http.rb b/hearth/lib/hearth/http.rb index dd374cbc8..2fa1965a4 100755 --- a/hearth/lib/hearth/http.rb +++ b/hearth/lib/hearth/http.rb @@ -3,10 +3,11 @@ require 'cgi' require_relative 'http/api_error' require_relative 'http/client' +require_relative 'http/error_inspector' require_relative 'http/error_parser' -require_relative 'http/headers' -require_relative 'http/middleware/content_length' -require_relative 'http/middleware/content_md5' +require_relative 'http/field' +require_relative 'http/fields' +require_relative 'http/middleware' require_relative 'http/networking_error' require_relative 'http/request' require_relative 'http/response' @@ -14,20 +15,28 @@ module Hearth # HTTP namespace for HTTP specific functionality. Also includes utility # methods for URI escaping. - # @api private module HTTP class << self # URI escapes the given value. # - # Hearth::_escape("a b/c") + # Hearth.uri_escape("a b/c") # #=> "a%20b%2Fc" # # @param [String] value # @return [String] URI encoded value except for '+' and '~'. + # @api private def uri_escape(value) CGI.escape(value.encode('UTF-8')).gsub('+', '%20').gsub('%7E', '~') end + # URI escapes the given path. + # + # Hearth.uri_escape_path("a b/c") + # #=> "a%20b/c" + # + # @param [String] path + # @return [String] URI encoded path except for '+' and '~'. + # @api private def uri_escape_path(path) path.gsub(%r{[^/]+}) { |part| uri_escape(part) } end diff --git a/hearth/lib/hearth/http/api_error.rb b/hearth/lib/hearth/http/api_error.rb index f88079808..25431ac82 100644 --- a/hearth/lib/hearth/http/api_error.rb +++ b/hearth/lib/hearth/http/api_error.rb @@ -7,7 +7,7 @@ module HTTP class ApiError < Hearth::ApiError def initialize(http_resp:, **kwargs) @http_status = http_resp.status - @http_headers = http_resp.headers + @http_fields = http_resp.fields @http_body = http_resp.body super(**kwargs) end @@ -15,8 +15,8 @@ def initialize(http_resp:, **kwargs) # @return [Integer] attr_reader :http_status - # @return [Hash] - attr_reader :http_headers + # @return [Fields] + attr_reader :http_fields # @return [String] attr_reader :http_body diff --git a/hearth/lib/hearth/http/client.rb b/hearth/lib/hearth/http/client.rb index abb1282ec..b69df27ba 100644 --- a/hearth/lib/hearth/http/client.rb +++ b/hearth/lib/hearth/http/client.rb @@ -1,109 +1,197 @@ # frozen_string_literal: true +require 'delegate' require 'net/http' require 'logger' require 'openssl' module Hearth module HTTP - # Transmits an HTTP {Request} object, returning an HTTP {Response}. - # @api private + # An HTTP client that uses Net::HTTP to send requests. class Client + # @api private + OPTIONS = { + logger: Logger.new($stdout), + debug_output: nil, + proxy: nil, + open_timeout: 15, + read_timeout: nil, + keep_alive_timeout: 5, + continue_timeout: 1, + write_timeout: nil, + ssl_timeout: nil, + verify_peer: true, + ca_file: nil, + ca_path: nil, + cert_store: nil, + host_resolver: nil + }.freeze + # Initialize an instance of this HTTP client. # # @param [Hash] options The options for this HTTP Client # - # @option options [Boolean] :http_wire_trace (false) When `true`, - # HTTP debug output will be sent to the `:logger`. + # @param options [Logger] (Logger.new($stdout)) :logger A logger + # used to log Net::HTTP requests and responses when `:debug_output` + # is enabled. # - # @option options [Logger] :logger A logger where debug output is sent. + # @option options [Boolean] :debug_output (false) When `true`, + # sets an output stream to the configured Logger for debugging. # - # @option options [URI::HTTP,String] :http_proxy A proxy to send + # @option options [String, URI] :proxy A proxy to send # requests through. Formatted like 'http://proxy.com:123'. # - # @option options [Boolean] :ssl_verify_peer (true) When `true`, + # @option options [Float] :open_timeout (15) Number of seconds to + # wait for the connection to open. + # + # @option options [Float] :read_timeout Number of seconds to wait + # for one block to be read (via one read(2) call). + # + # @option options [Float] :keep_alive_timeout (5) Seconds to reuse the + # connection of the previous request. + # + # @option options [Float] :continue_timeout (1) Seconds to wait for + # 100 Continue response. + # + # @option options [Float] :write_timeout Number of seconds to wait + # for one block to be written (via one write(2) call). + # + # @option options [Float] :ssl_timeout Sets the SSL timeout seconds. + # + # @option options [Boolean] :verify_peer (true) When `true`, # SSL peer certificates are verified when establishing a # connection. # - # @option options [String] :ssl_ca_bundle Full path to the SSL + # @option options [String] :ca_file Full path to the SSL # certificate authority bundle file that should be used when # verifying peer certificates. If you do not pass - # `:ssl_ca_bundle` or `:ssl_ca_directory` the system default + # `:ca_file` or `:ca_path` the system default # will be used if available. # - # @option options [String] :ssl_ca_directory Full path of the + # @option options [String] :ca_path Full path of the # directory that contains the unbundled SSL certificate # authority files for verifying peer certificates. If you do - # not pass `:ssl_ca_bundle` or `:ssl_ca_directory` the + # not pass `:ca_file` or `:ca_path` the # system default will be used if available. + # + # @option options [OpenSSL::X509::Store] :cert_store An OpenSSL X509 + # certificate store that contains the SSL certificate authority. + # + # @option options [#resolve_address] :host_resolver + # An object, such as {Hearth::DNS::HostResolver} that responds to + # `#resolve_address`, returning an array of up to two IP addresses for + # the given hostname, one IPv6 and one IPv4, in that order. + # `#resolve_address` should take a nodename keyword argument and + # optionally other keyword args similar to Addrinfo.getaddrinfo's + # positional parameters. def initialize(options = {}) - @http_wire_trace = options[:http_wire_trace] - @logger = options[:logger] - @http_proxy = options[:http_proxy] - @http_proxy = URI.parse(@http_proxy.to_s) if @http_proxy - @ssl_verify_peer = options[:ssl_verify_peer] - @ssl_ca_bundle = options[:ssl_ca_bundle] - @ssl_ca_directory = options[:ssl_ca_directory] - @ssl_ca_store = options[:ssl_ca_store] + OPTIONS.each_pair do |opt_name, default_value| + value = options.key?(opt_name) ? options[opt_name] : default_value + instance_variable_set("@#{opt_name}", value) + end + end + + OPTIONS.each_key do |attr_name| + attr_reader(attr_name) end # @param [Request] request # @param [Response] response + # @param [Logger] (nil) logger # @return [Response] - def transmit(request:, response:) - uri = URI.parse(request.url) - http = create_http(uri) - http.set_debug_output(@logger) if @http_wire_trace - - if uri.scheme == 'https' - configure_ssl(http) - else - http.use_ssl = false + def transmit(request:, response:, logger: nil) + net_request = build_net_request(request) + with_connection_pool(request.uri, logger) do |connection| + _transmit(connection, net_request, response) end - - _transmit(http, request, response) response.body.rewind if response.body.respond_to?(:rewind) response rescue ArgumentError => e # Invalid verb, ArgumentError is a StandardError raise e rescue StandardError => e - raise Hearth::HTTP::NetworkingError, e + Hearth::HTTP::NetworkingError.new(e) end private - def _transmit(http, request, response) - http.start do |conn| - conn.request(build_net_request(request)) do |net_resp| - response.status = net_resp.code.to_i - response.headers = extract_headers(net_resp) - net_resp.read_body do |chunk| - response.body.write(chunk) - end - end + def with_connection_pool(endpoint, logger) + pool = ConnectionPool.for(pool_config) + connection = pool.connection_for(endpoint) do + new_connection(endpoint, logger) + end + yield connection + pool.offer(endpoint, connection) + rescue StandardError => e + connection&.finish + raise e + end + + # Starts and returns a new HTTP connection. + # @param [URI] endpoint + # @return [Net::HTTP] + def new_connection(endpoint, logger) + http = create_http(endpoint) + http.set_debug_output(logger || @logger) if @debug_output + configure_timeouts(http) + + if endpoint.scheme == 'https' + configure_ssl(http) + else + http.use_ssl = false end + + http.start + http end - # Creates an HTTP connection to the endpoint - # Applies proxy if set + def _transmit(http, net_request, response) + # Inform monkey patch to use our DNS resolver + Thread.current[:net_http_hearth_dns_resolver] = @host_resolver + http.request(net_request) do |net_resp| + unpack_response(net_resp, response) + end + ensure + # Restore the default DNS resolver + Thread.current[:net_http_hearth_dns_resolver] = nil + end + + def unpack_response(net_resp, response) + response.status = net_resp.code.to_i + net_resp.each_header { |k, v| response.headers[k] = v } + net_resp.read_body do |chunk| + response.body.write(chunk) + end + end + + # Creates an HTTP connection to the endpoint. + # Applies proxy if set. def create_http(endpoint) args = [] args << endpoint.host args << endpoint.port - args += http_proxy_parts if @http_proxy + args += proxy_parts if @proxy # Net::HTTP.new uses positional arguments: host, port, proxy_args.... - Net::HTTP.new(*args.compact) + HTTP.new(Net::HTTP.new(*args.compact)) + end + + def configure_timeouts(http) + http.open_timeout = @open_timeout + http.keep_alive_timeout = @keep_alive_timeout + http.read_timeout = @read_timeout + http.continue_timeout = @continue_timeout + http.write_timeout = @write_timeout end # applies ssl settings to the HTTP object def configure_ssl(http) http.use_ssl = true - if @ssl_verify_peer + http.ssl_timeout = @ssl_timeout + if @verify_peer http.verify_mode = OpenSSL::SSL::VERIFY_PEER - http.ca_file = @ssl_ca_bundle if @ssl_ca_bundle - http.ca_path = @ssl_ca_directory if @ssl_ca_directory - http.cert_store = @ssl_ca_store if @ssl_ca_store + http.ca_file = @ca_file + http.ca_path = @ca_path + http.cert_store = @cert_store else http.verify_mode = OpenSSL::SSL::VERIFY_NONE end @@ -115,7 +203,7 @@ def configure_ssl(http) # @return [Net::HTTP::Request] def build_net_request(request) request_class = net_http_request_class(request) - req = request_class.new(request.url, request.headers.to_h) + req = request_class.new(request.uri.to_s, net_headers_for(request)) # Net::HTTP adds a default Content-Type when a body is present. # Set the body stream when it has an unknown size or when it is > 0. @@ -127,10 +215,16 @@ def build_net_request(request) req end - # @param [Net::HTTP::Response] response + # Validate that fields are not trailers and return a hash of headers. + # @param [HTTP::Request] request # @return [Hash] - def extract_headers(response) - response.to_hash.transform_values(&:first) + def net_headers_for(request) + # Trailers are not supported in Net::HTTP + if request.trailers.any? + raise NotImplementedError, 'Trailers are not supported in Net::HTTP' + end + + request.headers.to_h end # @param [Http::Request] request @@ -144,16 +238,69 @@ def net_http_request_class(request) raise ArgumentError, msg end - # Extract the parts of the http_proxy URI - # @return [Array(String)] - def http_proxy_parts + # Extract the parts of the proxy URI + # @return [Array] + def proxy_parts + proxy = URI(@proxy) [ - @http_proxy.host, - @http_proxy.port, - (@http_proxy.user && CGI.unescape(@http_proxy.user)), - (@http_proxy.password && CGI.unescape(@http_proxy.password)) + proxy.host, + proxy.port, + (proxy.user && CGI.unescape(proxy.user)), + (proxy.password && CGI.unescape(proxy.password)) ] end + + # Config options for the HTTP client used for connection pooling + # @return [Hash] + def pool_config + OPTIONS.each_key.with_object({}) do |option_name, hash| + hash[option_name] = instance_variable_get("@#{option_name}") + end + end + + # Helper methods extended onto Net::HTTP objects. + # @api private + class HTTP < Delegator + def initialize(http) + super(http) + @http = http + end + + # @return [Integer, nil] + attr_reader :last_used + + def __getobj__ + @http + end + + def __setobj__(obj) + @http = obj + end + + # Sends the request and tracks that this connection has been used. + def request(...) + @http.request(...) + @last_used = monotonic_milliseconds + end + + def stale? + @last_used.nil? || + (monotonic_milliseconds - @last_used) > keep_alive_timeout * 1000 + end + + # Attempts to close/finish the connection without raising an error. + def finish + @http.finish + rescue IOError + nil + end + + private + + def monotonic_milliseconds + Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) + end + end end end end diff --git a/hearth/lib/hearth/http/error_inspector.rb b/hearth/lib/hearth/http/error_inspector.rb new file mode 100644 index 000000000..b65d590b8 --- /dev/null +++ b/hearth/lib/hearth/http/error_inspector.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'time' + +module Hearth + module HTTP + # An HTTP error inspector, using hints from status code and headers. + # @api private + class ErrorInspector + def initialize(error, http_response) + @error = error + @http_response = http_response + end + + def retryable? + (modeled_retryable? || + throttling? || + transient? || + server?) && + @http_response.body.respond_to?(:truncate) + end + + def error_type + if transient? + 'Transient' + elsif throttling? + 'Throttling' + elsif server? + 'ServerError' + elsif client? + 'ClientError' + else + 'Unknown' + end + end + + def hints + hints = {} + if (retry_after = retry_after_hint) + hints[:retry_after_hint] = retry_after + end + hints + end + + private + + def transient? + @error.is_a?(Hearth::HTTP::NetworkingError) + end + + def throttling? + @http_response.status == 429 || modeled_throttling? + end + + def server? + (500..599).cover?(@http_response.status) + end + + def client? + (400..499).cover?(@http_response.status) + end + + def modeled_retryable? + @error.is_a?(Hearth::ApiError) && @error.retryable? + end + + def modeled_throttling? + modeled_retryable? && @error.throttling? + end + + def retry_after_hint + retry_after = @http_response.headers['retry-after'] + Integer(retry_after) + rescue ArgumentError # string is present, assume it is a date + begin + Time.parse(retry_after) - Time.now + rescue ArgumentError # empty string, somehow + nil + end + rescue TypeError # header is not prseent + nil + end + end + end +end diff --git a/hearth/lib/hearth/http/field.rb b/hearth/lib/hearth/http/field.rb new file mode 100644 index 000000000..ac9eb43e1 --- /dev/null +++ b/hearth/lib/hearth/http/field.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +module Hearth + module HTTP + # Represents an HTTP field. + # @api private + class Field + # @param [String] name The name of the field. + # @param [Array|#to_s] value (nil) The values for the field. It can be any + # object that responds to `#to_s` or an Array of objects that respond to + # `#to_s`. + # @param [Symbol] kind The kind of field, either :header or :trailer. + def initialize(name, value = nil, kind: :header) + if name.nil? || name.empty? + raise ArgumentError, 'Field name must be a non-empty String' + end + + @name = name + @value = value + @kind = kind + end + + # @return [String] + attr_reader :name + + # @return [Symbol] + attr_reader :kind + + # Returns an escaped string representation of the field. + # @return [String] + def value(encoding = nil) + value = + if @value.is_a?(Array) + @value.compact.map { |v| escape_value(v.to_s) }.join(', ') + else + @value.to_s + end + value = value.encode(encoding) if encoding + value + end + + # @return [Boolean] + def header? + @kind == :header + end + + # @return [Boolean] + def trailer? + @kind == :trailer + end + + def to_h + { @name => value } + end + + private + + def escape_value(str) + s = str + s.include?('"') || s.include?(',') ? "\"#{s.gsub('"', '\"')}\"" : s + end + end + end +end diff --git a/hearth/lib/hearth/http/fields.rb b/hearth/lib/hearth/http/fields.rb new file mode 100644 index 000000000..e98929573 --- /dev/null +++ b/hearth/lib/hearth/http/fields.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +module Hearth + module HTTP + # Provides Hash like access for Headers and Trailers with key normalization + # @api private + class Fields + include Enumerable + + # @param [Array] fields + # @param [String] encoding + def initialize(fields = [], encoding: 'utf-8') + unless fields.is_a?(Array) + raise ArgumentError, 'fields must be an Array' + end + + @entries = {} + fields.each { |field| self[field.name] = field } + @encoding = encoding + end + + # @return [String] + attr_reader :encoding + + # @param [String] key + def [](key) + @entries[key.downcase] + end + + # @param [String] key + # @param [Field] value + def []=(key, value) + raise ArgumentError, 'value must be a Field' unless value.is_a?(Field) + + @entries[key.downcase] = value + end + + # @param [String] key + # @return [Boolean] Returns `true` if there is a Field with the given key. + def key?(key) + @entries.key?(key.downcase) + end + + # @param [String] key + # @return [Field, nil] Returns the Field for the deleted Field key. + def delete(key) + @entries.delete(key.downcase) + end + + # @return [Enumerable] + def each(&block) + @entries.each(&block) + end + alias each_pair each + + # @return [Integer] Returns the number of Field entries. + def size + @entries.size + end + + # @return [Hash] + def clear + @entries = {} + end + + # Proxy class that wraps Fields to create Headers and Trailers + class Proxy + include Enumerable + + def initialize(fields, kind) + @fields = fields + @kind = kind + end + + # @param [String] key + def [](key) + @fields[key].value(@fields.encoding) if key?(key) + end + + # @param [String] key + # @param [#to_s, Array<#to_s>] value + def []=(key, value) + @fields[key] = Field.new(key, value, kind: @kind) + end + + # @param [String] key + # @return [Boolean] Returns `true` if there is a Field with the given + # key and kind. + def key?(key) + @fields.key?(key) && @fields[key].kind == @kind + end + + # @param [String] key + # @return [Field, nil] Returns the value for the deleted Field key. + def delete(key) + @fields.delete(key).value(@fields.encoding) if key?(key) + end + + # @return [Enumerable] + def each(&block) + @fields.filter { |_k, v| v.kind == @kind } + .to_h { |_k, v| [v.name, v.value(@fields.encoding)] } + .each(&block) + end + alias each_pair each + end + end + end +end diff --git a/hearth/lib/hearth/http/headers.rb b/hearth/lib/hearth/http/headers.rb deleted file mode 100755 index 5995270b8..000000000 --- a/hearth/lib/hearth/http/headers.rb +++ /dev/null @@ -1,84 +0,0 @@ -# frozen_string_literal: true - -module Hearth - module HTTP - # Provides Hash like access for Headers with key normalization - # @api private - class Headers - # @param [Hash] headers - def initialize(headers = {}) - @headers = {} - headers.each_pair do |key, value| - self[key] = value - end - end - - # @param [String] key - def [](key) - @headers[normalize(key)] - end - - # @param [String] key - # @param [String] value - def []=(key, value) - @headers[normalize(key)] = value.to_s - end - - # @param [String] key - # @return [Boolean] Returns `true` if there is a header with - # the given key. - def key?(key) - @headers.key?(normalize(key)) - end - - # @return [Array] - def keys - @headers.keys - end - - # @param [String] key - # @return [String, nil] Returns the value for the deleted key. - def delete(key) - @headers.delete(normalize(key)) - end - - # @return [Enumerable] - def each_pair(&block) - @headers.each(&block) - end - alias each each_pair - - # @return [Hash] - def to_hash - @headers.dup - end - alias to_h to_hash - - # @return [Integer] Returns the number of entries in the headers - # hash. - def size - @headers.size - end - - # @param [Hash] headers - # @return [Headers] - def update(headers) - headers.each_pair do |k, v| - self[k] = v - end - self - end - - # @return [Hash] - def clear - @headers = {} - end - - private - - def normalize(key) - key.to_s.gsub(/[^-]+/, &:capitalize) - end - end - end -end diff --git a/hearth/lib/hearth/http/middleware.rb b/hearth/lib/hearth/http/middleware.rb new file mode 100644 index 000000000..1b4596d1d --- /dev/null +++ b/hearth/lib/hearth/http/middleware.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require_relative 'middleware/content_length' +require_relative 'middleware/content_md5' + +module Hearth + module HTTP + # @api private + module Middleware; end + end +end diff --git a/hearth/lib/hearth/http/middleware/content_length.rb b/hearth/lib/hearth/http/middleware/content_length.rb index 4ce5532dd..9edea3614 100644 --- a/hearth/lib/hearth/http/middleware/content_length.rb +++ b/hearth/lib/hearth/http/middleware/content_length.rb @@ -15,7 +15,7 @@ def initialize(app, _ = {}) # @return [Output] def call(input, context) request = context.request - if request&.body.respond_to?(:size) && + if request.body.respond_to?(:size) && !request.headers.key?('Content-Length') length = request.body.size request.headers['Content-Length'] = length diff --git a/hearth/lib/hearth/http/networking_error.rb b/hearth/lib/hearth/http/networking_error.rb index d282e1807..7f1ab6866 100755 --- a/hearth/lib/hearth/http/networking_error.rb +++ b/hearth/lib/hearth/http/networking_error.rb @@ -2,19 +2,6 @@ module Hearth module HTTP - # Thrown by a Client when encountering a networking error while transmitting - # a request or receiving a response. You can access the original error - # by calling {#original_error}. - class NetworkingError < StandardError - MSG = 'Encountered an error while transmitting the request: %s' - - def initialize(original_error) - @original_error = original_error - super(format(MSG, message: original_error.message)) - end - - # @return [StandardError] - attr_reader :original_error - end + class NetworkingError < Hearth::NetworkingError; end end end diff --git a/hearth/lib/hearth/http/request.rb b/hearth/lib/hearth/http/request.rb index 5001ba770..96d5f4694 100755 --- a/hearth/lib/hearth/http/request.rb +++ b/hearth/lib/hearth/http/request.rb @@ -1,83 +1,77 @@ # frozen_string_literal: true -require 'stringio' -require 'uri' - module Hearth module HTTP # Represents an HTTP request. # @api private - class Request + class Request < Hearth::Request # @param [String] http_method - # @param [String] url - # @param [Headers] headers - # @param [IO] body - def initialize(http_method: nil, url: nil, headers: Headers.new, - body: StringIO.new) + # @param [Fields] fields + # @param (see Hearth::Request#initialize) + def initialize(http_method: nil, fields: Fields.new, **kwargs) + super(**kwargs) @http_method = http_method - @url = url - @headers = headers - @body = body + @fields = fields + @headers = Fields::Proxy.new(@fields, :header) + @trailers = Fields::Proxy.new(@fields, :trailer) end # @return [String] attr_accessor :http_method - # @return [String] - attr_accessor :url + # @return [Fields] + attr_reader :fields - # @return [Headers] - attr_accessor :headers + # @return [Fields::Proxy] + attr_reader :headers - # @return [IO] - attr_accessor :body + # @return [Fields::Proxy] + attr_reader :trailers - # Append a path to the HTTP request URL. + # Append a path to the HTTP request URI. # - # http_req.url = "https://example.com" + # http_req.uri = "https://example.com" # http_req.append_path('/') - # http_req.url + # http_req.uri.to_s # #=> "https://example.com/" # # Paths will be joined by a single '/': # - # http_req.url = "https://example.com/path-prefix/" + # http_req.uri = "https://example.com/path-prefix/" # http_req.append_path('/path-suffix') - # http_req.url + # http_req.uri.to_s # #=> "https://example.com/path-prefix/path-suffix" # - # Resultant URL preserves the querystring: + # Resultant URI preserves the querystring: # - # http_req.url = "https://example.com/path-prefix?querystring + # http_req.uri = "https://example.com/path-prefix?querystring # http_req.append_path('/path-suffix') - # http_req.url + # http_req.uri.to_s # #=> "https://example.com/path-prefix/path-suffix?querystring" # # The provided path should be URI escaped before being passed. # - # http_req.url = "https://example.com + # http_req.uri = "https://example.com # http_req.append_path( # Hearth::HTTP.uri_escape_path('/part 1/part 2') # ) - # http_req.url + # http_req.uri.to_s # #=> "https://example.com/part%201/part%202" # # @param [String] path A URI escaped path. def append_path(path) - uri = URI.parse(@url) base_path = uri.path.sub(%r{/$}, '') # remove trailing slash path = path.sub(%r{^/}, '') # remove prefix slash uri.path = "#{base_path}/#{path}" # join on single slash - @url = uri.to_s end - # Append querystring parameter to the HTTP request URL. + # Append querystring parameter to the HTTP request URI. # - # http_req.url = "https://example.com" + # http_req.uri = "https://example.com" # http_req.append_query_param('query') # http_req.append_query_param('key 1', 'value 1') # - # http_req.url + # http_req.uri.to_s # #=> "https://example.com?query&key%201=value%201 # # @overload append_query_param(name) @@ -96,57 +90,45 @@ def append_path(path) def append_query_param(*args) param = case args.size - when 1 then escape(args[0]) - when 2 then "#{escape(args[0])}=#{escape(args[1])}" + when 1 then Hearth::Query::Param.new(args[0]) + when 2 then Hearth::Query::Param.new(args[0], args[1]) else raise ArgumentError, 'wrong number of arguments ' \ "(given #{args.size}, expected 1 or 2)" end - uri = URI.parse(@url) - uri.query = uri.query ? "#{uri.query}&#{param}" : param - @url = uri.to_s + uri.query = uri.query ? "#{uri.query}&#{param}" : param.to_s end - # Append querystring parameters to the HTTP request URL. + # Append querystring parameter list to the HTTP request URI. # - # http_req.url = "https://example.com" + # http_req.uri = "https://example.com" # query_params = Hearth::Query::ParamList.new # query_params['key 1'] = nil # query_params['key 2'] = 'value 2' - # http_req.append_query_params(query_params) + # http_req.append_query_param_list(query_params) # - # http_req.url + # http_req.uri.to_s # #=> "https://example.com?key%201=&key%202=value%202" # # @param [ParamList] param_list # An instance of Hearth::Query::ParamList containing the list of # querystring parameters to add. The names and values are URI escaped. # - def append_query_params(param_list) - uri = URI.parse(@url) + def append_query_param_list(param_list) uri.query = uri.query ? "#{uri.query}&#{param_list}" : param_list.to_s - @url = uri.to_s end - # Append a host prefix to the HTTP request URL. + # Append a host prefix to the HTTP request URI. # - # http_req.url = "https://example.com" + # http_req.uri = "https://example.com" # http_req.prefix_host('data.') # - # http_req.url + # http_req.uri.to_s # #=> "https://data.foo.com # # @param [String] prefix A dot (.) terminated prefix for the host. # def prefix_host(prefix) - uri = URI.parse(@url) uri.host = prefix + uri.host - @url = uri.to_s - end - - private - - def escape(value) - Hearth::HTTP.uri_escape(value.to_s) end end end diff --git a/hearth/lib/hearth/http/response.rb b/hearth/lib/hearth/http/response.rb index 3003d4d2a..4bf1ab069 100755 --- a/hearth/lib/hearth/http/response.rb +++ b/hearth/lib/hearth/http/response.rb @@ -1,35 +1,44 @@ # frozen_string_literal: true -require 'stringio' - module Hearth module HTTP # Represents an HTTP Response. # @api private - class Response + class Response < Hearth::Response # @param [Integer] status - # @param [Headers] headers - # @param [IO] body - def initialize(status: 0, headers: Headers.new, body: StringIO.new) + # @param [String, nil] reason + # @param [Fields] fields + # @param (see Hearth::Response#initialize) + def initialize(status: 0, reason: nil, fields: Fields.new, **kwargs) + super(**kwargs) @status = status - @headers = headers - @body = body + @reason = reason + @fields = fields + @headers = Fields::Proxy.new(@fields, :header) + @trailers = Fields::Proxy.new(@fields, :trailer) end # @return [Integer] attr_accessor :status - # @return [Headers] - attr_accessor :headers + # @return [String, nil] + attr_accessor :reason + + # @return [Fields] + attr_reader :fields + + # @return [Fields::Proxy] + attr_reader :headers - # @return [IO] - attr_accessor :body + # @return [Fields::Proxy] + attr_reader :trailers # Resets the HTTP response. # @return [Response] def reset @status = 0 - @headers.clear + @reason = nil + @fields.clear @body.truncate(0) self end diff --git a/hearth/lib/hearth/middleware/retry.rb b/hearth/lib/hearth/middleware/retry.rb index 272773eae..fbd997c2c 100755 --- a/hearth/lib/hearth/middleware/retry.rb +++ b/hearth/lib/hearth/middleware/retry.rb @@ -2,114 +2,60 @@ module Hearth module Middleware - # A middleware that retries the request. + # A middleware that retries the request using a retry strategy. # @api private class Retry - # Max backoff (in seconds) - MAX_BACKOFF = 20 - # @param [Class] app The next middleware in the stack. - # @param [Boolean] retry_mode Specifies which retry algorithm to use. - # Values are: - # * `standard` - A standardized set of retry rules across the AWS SDKs. - # This includes support for retry quotas, which limit the number of - # unsuccessful retries a client can make. - # * `adaptive` - An experimental retry mode that includes all the - # functionality of `standard` mode along with automatic client side - # throttling. This is a provisional mode that may change behavior - # in the future. - # @param [String] max_attempts An integer representing the maximum number - # of attempts that will be made for a single request, including the - # initial attempt. - # @param [Boolean] adaptive_retry_wait_to_fill Used only in `adaptive` - # retry mode. When true, the request will sleep until there is - # sufficient client side capacity to retry the request. When false, - # the request will raise a `CapacityNotAvailableError` and will not - # retry instead of sleeping. - def initialize(app, retry_mode:, max_attempts:, - adaptive_retry_wait_to_fill:, error_inspector_class:, - retry_quota:, client_rate_limiter:) + # @param [Standard|Adaptive] retry_strategy (Standard) The retry strategy + # to use. Hearth has two built in classes, Standard and Adaptive. + # * `Retry::Standard` - A standardized set of retry rules across + # the AWS SDKs. This includes support for retry quotas, which limit + # the number of unsuccessful retries a client can make. + # * `Retry::Adaptive` - An experimental retry mode that includes + # all the functionality of `standard` mode along with automatic + # client side throttling. This is a provisional mode that may change + # behavior in the future. + def initialize(app, retry_strategy:, error_inspector_class:) @app = app - # public config - @retry_mode = retry_mode - @max_attempts = max_attempts - @adaptive_retry_wait_to_fill = adaptive_retry_wait_to_fill - - # undocumented options + @retry_strategy = retry_strategy + # undocumented - protocol specific @error_inspector_class = error_inspector_class - @retry_quota = retry_quota - @client_rate_limiter = client_rate_limiter - # instance state - @capacity_amount = nil @retries = 0 end - # @param input - # @param context - # @return [Output] - # rubocop:disable Metrics/AbcSize def call(input, context) - acquire_token - output = @app.call(input, context) - error_inspector = @error_inspector_class.new( - output.error, context.response.status + token = @retry_strategy.acquire_initial_retry_token( + context.metadata[:retry_token_scope] ) - request_bookkeeping(output, error_inspector) - return output unless retryable?(context, output, error_inspector) + output = nil + loop do + output = @app.call(input, context) - return output if @retries >= @max_attempts - 1 + if (error = output.error) + error_info = @error_inspector_class.new(error, context.response) + token = @retry_strategy.refresh_retry_token(token, error_info) - @capacity_amount = @retry_quota.checkout_capacity(error_inspector) - return output unless @capacity_amount.positive? + Kernel.sleep(token.retry_delay) if token + else + @retry_strategy.record_success(token) + end + break unless token && output.error - delay = [Kernel.rand * (2**@retries), MAX_BACKOFF].min - Kernel.sleep(delay) - retry_request(input, context, output) + reset_request(context, output) + @retries += 1 + end + output end - # rubocop:enable Metrics/AbcSize private - def acquire_token - return if @retry_mode == 'standard' - - # either fail fast or block until a token becomes available - # must be configurable - # need a maximum rate at which we can send requests (max_send_rate) - # is unset until a throttle is seen - @client_rate_limiter.token_bucket_acquire( - 1, wait_to_fill: @adaptive_retry_wait_to_fill - ) - end - - # max_send_rate is updated if on adaptive mode and based on response - # retry quota is updated if the request is successful (both modes) - def request_bookkeeping(output, error_inspector) - @retry_quota.release(@capacity_amount) unless output.error - - return unless @retry_mode == 'adaptive' - - @client_rate_limiter.update_sending_rate( - error_inspector.error_type == 'Throttling' - ) - end - - def retryable?(context, output, error_inspector) - return false unless output.error - - error_inspector.retryable? && - context.response.body.respond_to?(:truncate) - end - - def retry_request(input, context, output) - @retries += 1 + def reset_request(context, output) context.request.body.rewind context.response.reset output.error = nil - call(input, context) end end end diff --git a/hearth/lib/hearth/middleware/send.rb b/hearth/lib/hearth/middleware/send.rb index f3acf9bc5..0b23dfe10 100755 --- a/hearth/lib/hearth/middleware/send.rb +++ b/hearth/lib/hearth/middleware/send.rb @@ -28,64 +28,31 @@ def initialize(_app, client:, stub_responses:, # @param context # @return [Output] def call(input, context) - - # modify_before_transmit hook - # exception behavior - exceptions set to output.error and control - # bubbles up to modifyBeforeAttemptCompletion - context.interceptors.reverse.each do |i| - if i.respond_to?(:modify_before_transmit) - begin - i.modify_before_transmit(context.interceptor_context(input, nil)) - rescue StandardError => e - return Hearth::Output.new(error: e) - end - end - end - - # read_before_transmit hook - # exception behavior - exceptions set to output.error and control - # bubbles up to modifyBeforeAttemptCompletion - context.interceptors.reverse.each do |i| - if i.respond_to?(:read_before_transmit) - begin - i.read_before_transmit(context.interceptor_context(input, nil)) - rescue StandardError => e - return Hearth::Output.new(error: e) - end - end - end - + # TODO: Add modify and read_before_transmit hooks + output = Output.new if @stub_responses stub = @stubs.next(context.operation_name) - output = Output.new apply_stub(stub, input, context, output) + if context.response.body.respond_to?(:rewind) + context.response.body.rewind + end else - @client.transmit( + # TODO: should this instead raise NetworkingError? + resp_or_error = @client.transmit( request: context.request, - response: context.response + response: context.response, + logger: context.logger ) - output = Output.new - end - - # read_after_transmit hook - # exception behavior - exceptions set to output.error and control - # bubbles up to modifyBeforeAttemptCompletion - context.interceptors.reverse.each do |i| - if i.respond_to?(:read_after_transmit) - begin - i.read_after_transmit(context.interceptor_context(input, output)) - rescue StandardError => e - return Hearth::Output.new(error: e) - end + if resp_or_error.is_a?(Hearth::NetworkingError) + output.error = resp_or_error end end - + # TODO: Add read after transmit hook output end private - # rubocop:disable Metrics/MethodLength def apply_stub(stub, input, context, output) case stub when Proc @@ -115,7 +82,6 @@ def apply_stub(stub, input, context, output) raise ArgumentError, 'Unsupported stub type' end end - # rubocop:enable Metrics/MethodLength end end end diff --git a/hearth/lib/hearth/middleware_builder.rb b/hearth/lib/hearth/middleware_builder.rb index 10dc94ec8..acefb10c1 100755 --- a/hearth/lib/hearth/middleware_builder.rb +++ b/hearth/lib/hearth/middleware_builder.rb @@ -205,21 +205,21 @@ def remove(klass) %w[before after around].each do |method| method_name = "#{method}_#{simple_step_name}" define_method(method_name) do |*args, &block| - return send(method, klass, *args, &block) + send(method, klass, *args, &block) end define_singleton_method(method_name) do |*args, &block| - return send(method, klass, *args, &block) + send(method, klass, *args, &block) end end remove_method_name = "remove_#{simple_step_name}" define_method(remove_method_name) do - return remove(klass) + remove(klass) end define_singleton_method(remove_method_name) do - return remove(klass) + remove(klass) end end diff --git a/hearth/lib/hearth/networking_error.rb b/hearth/lib/hearth/networking_error.rb new file mode 100644 index 000000000..780a40d07 --- /dev/null +++ b/hearth/lib/hearth/networking_error.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Hearth + # Thrown by a Client when encountering a networking error while transmitting + # a request or receiving a response. You can access the original error + # by calling {#original_error}. + class NetworkingError < StandardError + MSG = 'Encountered an error while transmitting the request: %s' + + def initialize(original_error) + @original_error = original_error + super(format(MSG, message: original_error.message)) + end + + # @return [StandardError] + attr_reader :original_error + end +end diff --git a/hearth/lib/hearth/query/param.rb b/hearth/lib/hearth/query/param.rb index 6d6e0aad3..5a127de4f 100644 --- a/hearth/lib/hearth/query/param.rb +++ b/hearth/lib/hearth/query/param.rb @@ -43,7 +43,7 @@ def <=>(other) private def serialize(name, value) - value.nil? ? "#{escape(name)}=" : "#{escape(name)}=#{escape(value)}" + value.nil? ? escape(name) : "#{escape(name)}=#{escape(value)}" end def escape(value) diff --git a/hearth/lib/hearth/request.rb b/hearth/lib/hearth/request.rb new file mode 100644 index 000000000..9b87cc029 --- /dev/null +++ b/hearth/lib/hearth/request.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'stringio' +require 'uri' + +module Hearth + # Represents a base request. + # @api private + class Request + # @param [URI] uri (URI('')) + # @param [IO] body (StringIO.new) + def initialize(uri: URI(''), body: StringIO.new) + @uri = uri + @body = body + end + + # @return [URI] + attr_accessor :uri + + # @return [IO] + attr_accessor :body + end +end diff --git a/hearth/lib/hearth/response.rb b/hearth/lib/hearth/response.rb new file mode 100644 index 000000000..2451f5e36 --- /dev/null +++ b/hearth/lib/hearth/response.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'stringio' + +module Hearth + # Represents a base response. + # @api private + class Response + # @param [IO] body (StringIO.new) + def initialize(body: StringIO.new) + @body = body + end + + # @return [IO] + attr_accessor :body + end +end diff --git a/hearth/lib/hearth/retry.rb b/hearth/lib/hearth/retry.rb index 44981d16a..22682064a 100644 --- a/hearth/lib/hearth/retry.rb +++ b/hearth/lib/hearth/retry.rb @@ -1,5 +1,15 @@ # frozen_string_literal: true +require_relative 'retry/strategy' + +require_relative 'retry/adaptive' require_relative 'retry/client_rate_limiter' -require_relative 'retry/error_inspector' +require_relative 'retry/exponential_backoff' require_relative 'retry/retry_quota' +require_relative 'retry/standard' + +module Hearth + module Retry + Token = Struct.new(:retry_count, :retry_delay, keyword_init: true) + end +end diff --git a/hearth/lib/hearth/retry/adaptive.rb b/hearth/lib/hearth/retry/adaptive.rb new file mode 100644 index 000000000..0f7612196 --- /dev/null +++ b/hearth/lib/hearth/retry/adaptive.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Hearth + module Retry + # Adaptive retry strategy for retrying requests. + class Adaptive < Strategy + # @param [#call] backoff (ExponentialBackoff) A callable object that + # calculates a backoff delay for a retry attempt. + # @param [Integer] max_attempts (3) The maximum number of attempts that + # will be made for a single request, including the initial attempt. + # @param [Boolean] wait_to_fill When true, the request will sleep until + # there is sufficient client side capacity to retry the request. When + # false, the request will raise a `CapacityNotAvailableError` and will + # not retry instead of sleeping. + def initialize(backoff: ExponentialBackoff.new, max_attempts: 3, + wait_to_fill: true) + super() + @backoff = backoff + @max_attempts = max_attempts + @wait_to_fill = wait_to_fill + + # instance state + @client_rate_limiter = ClientRateLimiter.new + @retry_quota = RetryQuota.new + @capacity_amount = nil + end + + def acquire_initial_retry_token(_token_scope = nil) + @client_rate_limiter.token_bucket_acquire( + 1, wait_to_fill: @wait_to_fill + ) + Token.new(retry_count: 0) + end + + def refresh_retry_token(retry_token, error_info) + return unless error_info.retryable? + + @client_rate_limiter.update_sending_rate( + error_info.error_type == 'Throttling' + ) + return if retry_token.retry_count >= @max_attempts - 1 + + @capacity_amount = @retry_quota.checkout_capacity(error_info) + return unless @capacity_amount.positive? + + delay = error_info.hints[:retry_after_hint] + delay ||= @backoff.call(retry_token.retry_count) + retry_token.retry_count += 1 + retry_token.retry_delay = delay + retry_token + end + + def record_success(retry_token) + @client_rate_limiter.update_sending_rate(false) + @retry_quota.release(@capacity_amount) + retry_token + end + end + end +end diff --git a/hearth/lib/hearth/retry/client_rate_limiter.rb b/hearth/lib/hearth/retry/client_rate_limiter.rb index 938b4ea5c..4fbf74d8e 100644 --- a/hearth/lib/hearth/retry/client_rate_limiter.rb +++ b/hearth/lib/hearth/retry/client_rate_limiter.rb @@ -50,7 +50,6 @@ def token_bucket_acquire(amount, wait_to_fill: true) end end - # rubocop:disable Metrics/MethodLength def update_sending_rate(is_throttling_error) @mutex.synchronize do update_measured_rate @@ -77,7 +76,6 @@ def update_sending_rate(is_throttling_error) token_bucket_update_rate(new_rate) end end - # rubocop:enable Metrics/MethodLength private diff --git a/hearth/lib/hearth/retry/error_inspector.rb b/hearth/lib/hearth/retry/error_inspector.rb deleted file mode 100644 index af56da3bb..000000000 --- a/hearth/lib/hearth/retry/error_inspector.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -module Hearth - module Retry - # @api private - class ErrorInspector - def initialize(error, http_status) - @error = error - @http_status = http_status - end - - def retryable? - modeled_retryable? || - throttling? || - networking? || - server? - end - - def error_type - if networking? - 'Transient' - elsif throttling? - 'Throttling' - elsif server? - 'ServerError' - elsif client? - 'ClientError' - else - 'Unknown' - end - end - - def throttling? - @http_status == 429 || modeled_throttling? - end - - def networking? - @error.is_a?(Hearth::HTTP::NetworkingError) - end - - def server? - (500..599).cover?(@http_status) - end - - def client? - (400..499).cover?(@http_status) - end - - def modeled_retryable? - @error.is_a?(Hearth::ApiError) && @error.retryable? - end - - def modeled_throttling? - @error.is_a?(Hearth::ApiError) && @error.throttling? - end - end - end -end diff --git a/hearth/lib/hearth/retry/exponential_backoff.rb b/hearth/lib/hearth/retry/exponential_backoff.rb new file mode 100644 index 000000000..22a331f9e --- /dev/null +++ b/hearth/lib/hearth/retry/exponential_backoff.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Hearth + module Retry + # @api private + # Computes an exponential backoff delay for a retry attempt. + class ExponentialBackoff + # Max backoff (in seconds) + MAX_BACKOFF = 20 + + def call(attempts) + [Kernel.rand * (2**attempts), MAX_BACKOFF].min + end + end + end +end diff --git a/hearth/lib/hearth/retry/retry_quota.rb b/hearth/lib/hearth/retry/retry_quota.rb index 631380213..3b0a5527f 100644 --- a/hearth/lib/hearth/retry/retry_quota.rb +++ b/hearth/lib/hearth/retry/retry_quota.rb @@ -19,13 +19,14 @@ def initialize # Check if there is sufficient capacity to retry and return it. # If there is insufficient capacity, return 0 # @return [Integer] The amount of capacity checked out - def checkout_capacity(error_inspector) + def checkout_capacity(error_info) @mutex.synchronize do - capacity_amount = if error_inspector.error_type == 'Transient' - TIMEOUT_RETRY_COST - else - RETRY_COST - end + capacity_amount = + if error_info.error_type == 'Transient' + TIMEOUT_RETRY_COST + else + RETRY_COST + end # unable to acquire capacity return 0 if capacity_amount > @available_capacity diff --git a/hearth/lib/hearth/retry/standard.rb b/hearth/lib/hearth/retry/standard.rb new file mode 100644 index 000000000..3903d196d --- /dev/null +++ b/hearth/lib/hearth/retry/standard.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Hearth + module Retry + # Standard retry strategy for retrying requests. + class Standard < Strategy + # @param [#call] backoff (ExponentialBackoff) A callable object that + # calculates a backoff delay for a retry attempt. + # @param [Integer] max_attempts (3) The maximum number of attempts that + # will be made for a single request, including the initial attempt. + def initialize(backoff: ExponentialBackoff.new, max_attempts: 3) + super() + @backoff = backoff + @max_attempts = max_attempts + + # instance state + @retry_quota = RetryQuota.new + @capacity_amount = nil + end + + def acquire_initial_retry_token(_token_scope = nil) + Token.new(retry_count: 0) + end + + def refresh_retry_token(retry_token, error_info) + return unless error_info.retryable? + + return if retry_token.retry_count >= @max_attempts - 1 + + @capacity_amount = @retry_quota.checkout_capacity(error_info) + return unless @capacity_amount.positive? + + delay = error_info.hints[:retry_after_hint] + delay ||= @backoff.call(retry_token.retry_count) + retry_token.retry_count += 1 + retry_token.retry_delay = delay + retry_token + end + + def record_success(retry_token) + @retry_quota.release(@capacity_amount) + retry_token + end + end + end +end diff --git a/hearth/lib/hearth/retry/strategy.rb b/hearth/lib/hearth/retry/strategy.rb new file mode 100644 index 000000000..7ab221f52 --- /dev/null +++ b/hearth/lib/hearth/retry/strategy.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Hearth + module Retry + # Interface for retry strategies. + class Strategy + def acquire_initial_retry_token(_token_scope = nil) + raise NotImplementedError + end + + def refresh_retry_token(_retry_token, _error_info) + raise NotImplementedError + end + + def record_success(_retry_token) + raise NotImplementedError + end + end + end +end diff --git a/hearth/lib/hearth/time_helper.rb b/hearth/lib/hearth/time_helper.rb index 2797cb98c..168f11208 100755 --- a/hearth/lib/hearth/time_helper.rb +++ b/hearth/lib/hearth/time_helper.rb @@ -11,7 +11,8 @@ class << self # @param [Time] time # @return [String] The time as an ISO8601 string. def to_date_time(time) - time.utc.strftime('%Y-%m-%dT%H:%M:%SZ') + optional_ms_digits = time.subsec.zero? ? nil : 3 + time.utc.iso8601(optional_ms_digits) end # @param [Time] time @@ -28,7 +29,8 @@ def to_epoch_seconds(time) # @return [String] Returns the time formatted # as an HTTP header date. def to_http_date(time) - time.utc.httpdate + fractional = '.%L' unless time.subsec.zero? + time.utc.strftime("%a, %d %b %Y %H:%M:%S#{fractional} GMT") end end end diff --git a/hearth/sig/lib/hearth/http/client.rbs b/hearth/sig/lib/hearth/http/client.rbs new file mode 100644 index 000000000..33392d642 --- /dev/null +++ b/hearth/sig/lib/hearth/http/client.rbs @@ -0,0 +1,100 @@ +module Hearth + module HTTP + # An HTTP client that uses Net::HTTP to send requests. + class Client + # Initialize an instance of this HTTP client. + # + # @param [Hash] options The options for this HTTP Client + # + # @option options [Boolean] :debug_output (false) When `true`, + # sets an output stream to the configured Logger for debugging. + # + # @option options [String] :proxy A proxy to send + # requests through. Formatted like 'http://proxy.com:123'. + # + # @option options [Float] :open_timeout Number of seconds to + # wait for the connection to open. + # + # @option options [Float] :read_timeout Number of seconds to wait + # for one block to be read (via one read(2) call). + # + # @option options [Float] :keep_alive_timeout Seconds to reuse the + # connection of the previous request. + # + # @option options [Float] :continue_timeout Seconds to wait for + # 100 Continue response. + # + # @option options [Float] :write_timeout Number of seconds to wait + # for one block to be written (via one write(2) call). + # + # @option options [Float] :ssl_timeout Sets the SSL timeout seconds. + # + # @option options [Boolean] :verify_peer (true) When `true`, + # SSL peer certificates are verified when establishing a + # connection. + # + # @option options [String] :ca_file Full path to the SSL + # certificate authority bundle file that should be used when + # verifying peer certificates. If you do not pass + # `:ca_file` or `:ca_path` the system default + # will be used if available. + # + # @option options [String] :ca_path Full path of the + # directory that contains the unbundled SSL certificate + # authority files for verifying peer certificates. If you do + # not pass `:ca_file` or `:ca_path` the + # system default will be used if available. + # + # @option options [OpenSSL::X509::Store] :cert_store An OpenSSL X509 + # certificate store that contains the SSL certificate authority. + # + # @option options [#resolve_address] (nil) :host_resolver + # An object, such as {Hearth::DNS::HostResolver} that responds to + # `#resolve_address`, returning an array of up to two IP addresses for + # the given hostname, one IPv6 and one IPv4, in that order. + # `#resolve_address` should take a nodename keyword argument and + # optionally other keyword args similar to {Addrinfo#getaddrinfo}'s + # positional parameters. + def initialize: (?::Hash[untyped, untyped] options) -> void + + # @param [Request] request + # @param [Response] response + # @return [Response] + def transmit: (request: untyped, response: untyped, **untyped options) -> untyped + + private + + def _transmit: (untyped http, untyped request, untyped response) -> untyped + + def unpack_response: (untyped net_resp, untyped response) -> untyped + + # Creates an HTTP connection to the endpoint + # Applies proxy if set + def create_http: (untyped endpoint) -> untyped + + # applies ssl settings to the HTTP object + def configure_ssl: (untyped http) -> untyped + + # Constructs and returns a Net::HTTP::Request object from + # a {Http::Request}. + # @param [Http::Request] request + # @return [Net::HTTP::Request] + def build_net_request: (untyped request) -> untyped + + # Validate that fields are not trailers and return a hash of headers. + # @param [HTTP::Request] request + # @return [Hash] + def net_headers_for: (untyped request) -> untyped + + # @param [Http::Request] request + # @raise [InvalidHttpVerbError] + # @return Returns a base `Net::HTTP::Request` class, e.g., + # `Net::HTTP::Get`, `Net::HTTP::Post`, etc. + def net_http_request_class: (untyped request) -> untyped + + # Extract the parts of the proxy URI + # @return [Array] + def proxy_parts: () -> ::Array[untyped] + end + end +end diff --git a/hearth/sig/lib/hearth/http/headers.rbs b/hearth/sig/lib/hearth/http/headers.rbs deleted file mode 100644 index 80af81377..000000000 --- a/hearth/sig/lib/hearth/http/headers.rbs +++ /dev/null @@ -1,47 +0,0 @@ -module Hearth - module HTTP - # Provides Hash like access for Headers with key normalization - # @api private - class Headers - # @param [Hash] headers - def initialize: (?::Hash[String, String] headers) -> Headers - - # @param [String] key - def []: (String key) -> String - - # @param [String] key - # @param [String] value - def []=: (String key, String value) -> String - - # @param [String] key - # @return [Boolean] Returns `true` if there is a header with - # the given key. - def key?: (String key) -> bool - - # @return [Array] - def keys: () -> Array[String] - - # @param [String] key - # @return [String, nil] Returns the value for the deleted key. - def delete: (String key) -> (String | nil) - - # @return [Enumerable] - def each_pair: () { () -> String } -> Enumerable[Array[String]] - - alias each each_pair - - # @return [Hash] - def to_hash: () -> Hash[String, String] - - alias to_h to_hash - - # @return [Integer] Returns the number of entries in the headers - # hash. - def size: () -> Integer - - private - - def normalize: (String key) -> String - end - end -end diff --git a/hearth/sig/lib/hearth/http/response.rbs b/hearth/sig/lib/hearth/http/response.rbs index beb9d3f4c..660c40d9b 100644 --- a/hearth/sig/lib/hearth/http/response.rbs +++ b/hearth/sig/lib/hearth/http/response.rbs @@ -2,20 +2,31 @@ module Hearth module HTTP # Represents an HTTP Response. # @api private - class Response + class Response < Hearth::Response # @param [Integer] status - # @param [Headers] headers - # @param [IO] body - def initialize: (?status: ::Integer status, ?headers: Headers headers, ?body: IO body) -> Response + # @param [String, nil] reason + # @param [Fields] fields + # @param (see Hearth::Response#initialize) + def initialize: (?status: ::Integer, ?reason: untyped?, ?fields: untyped, **untyped kwargs) -> void # @return [Integer] - attr_accessor status: Integer + attr_accessor status: untyped - # @return [Headers] - attr_accessor headers: Headers + # @return [String, nil] + attr_accessor reason: untyped - # @return [IO] - attr_accessor body: IO + # @return [Fields] + attr_reader fields: untyped + + # @return [Fields::Proxy] + attr_reader headers: untyped + + # @return [Fields::Proxy] + attr_reader trailers: untyped + + # Resets the HTTP response. + # @return [Response] + def reset: () -> self end end -end +end \ No newline at end of file diff --git a/hearth/sig/lib/hearth/response.rbs b/hearth/sig/lib/hearth/response.rbs new file mode 100644 index 000000000..00f236cdb --- /dev/null +++ b/hearth/sig/lib/hearth/response.rbs @@ -0,0 +1,11 @@ +module Hearth + # Represents a base response. + # @api private + class Response + # @param [IO] body (StringIO.new) + def initialize: (?body: untyped) -> void + + # @return [IO] + attr_accessor body: untyped + end +end diff --git a/hearth/spec/hearth/connection_pool_spec.rb b/hearth/spec/hearth/connection_pool_spec.rb new file mode 100644 index 000000000..f49b4abcc --- /dev/null +++ b/hearth/spec/hearth/connection_pool_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +module Hearth + describe ConnectionPool do + # Set instance variable instead of calling empty! + # to avoid re-use of double rspec error. + before do + ConnectionPool.instance_variable_set(:@pools, {}) + end + + let(:config) { { timeout: 1 } } + let(:pool) { ConnectionPool.for(config) } + + let(:endpoint) { URI('https://example.com') } + let(:endpoint2) { URI('https://example.org') } + let(:endpoint_path_query) { URI('https://example.com/path?query') } + let(:connection) { double('connection', stale?: false, finish: nil) } + let(:connection2) { double('connection2', stale?: false, finish: nil) } + let(:stale_connection) { double('stale_connection', stale?: true) } + + describe '.for' do + it 'returns a connection pool' do + expect(ConnectionPool.for(config)).to be_a(ConnectionPool) + end + + it 'returns the same connection pool for the same config' do + expect(ConnectionPool.for(config)).to eq(ConnectionPool.for(config)) + end + + it 'returns a different connection pool for a different config' do + expect(ConnectionPool.for(config)).not_to eq(ConnectionPool.for({})) + end + end + + describe '.pools' do + it 'returns a list of the constructed connection pools' do + expect(ConnectionPool.pools).to be_a(Array) + end + + it 'returns the same list of constructed connection pools' do + ConnectionPool.for(config) + expect(ConnectionPool.pools).to eq(ConnectionPool.pools) + end + end + + describe '#offer / #connection_for' do + it 'returns a new default connection using a block' do + actual = pool.connection_for(endpoint) { connection } + expect(actual).to eq(connection) + end + + it 'ignores the block if there is a connection' do + pool.offer(endpoint, connection) + actual = pool.connection_for(endpoint) { connection2 } + expect(actual).to eq(connection) + end + + it 'is keyed by endpoint' do + pool.offer(endpoint, connection) + pool.offer(endpoint2, connection2) + actual = pool.connection_for(endpoint) + actual2 = pool.connection_for(endpoint2) + expect(actual).to eq(connection) + expect(actual2).to eq(connection2) + end + + it 'uses FIFO order' do + pool.offer(endpoint, connection) + pool.offer(endpoint, connection2) + actual = pool.connection_for(endpoint) + expect(actual).to eq(connection) + actual = pool.connection_for(endpoint) + expect(actual).to eq(connection2) + end + + it 'removes stale connections' do + pool.offer(endpoint, stale_connection) + pool.offer(endpoint, connection) + actual = pool.connection_for(endpoint) + expect(actual).to eq(connection) + end + + it 'uses the same endpoint without path and query' do + pool.offer(endpoint_path_query, connection) + actual = pool.connection_for(endpoint) + expect(actual).to eq(connection) + end + end + + describe '#empty!' do + it 'closes and removes all sessions from the pool' do + pool.offer(endpoint, connection) + pool.offer(endpoint2, connection2) + expect(connection).to receive(:finish) + expect(connection2).to receive(:finish) + pool.empty! + expect(pool.connection_for(endpoint)).to be_nil + expect(pool.connection_for(endpoint2)).to be_nil + end + end + end +end diff --git a/hearth/spec/hearth/dns/host_address_spec.rb b/hearth/spec/hearth/dns/host_address_spec.rb new file mode 100644 index 000000000..e87f46852 --- /dev/null +++ b/hearth/spec/hearth/dns/host_address_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Hearth + module DNS + describe HostAddress do + it 'can be initialized' do + HostAddress.new( + address_type: :A, + address: '123.123.123.123', + hostname: 'example.com' + ) + end + end + end +end diff --git a/hearth/spec/hearth/dns/host_resolver_spec.rb b/hearth/spec/hearth/dns/host_resolver_spec.rb new file mode 100644 index 000000000..61ec80caa --- /dev/null +++ b/hearth/spec/hearth/dns/host_resolver_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +module Hearth + module DNS + describe HostResolver do + let(:nodename) { 'example.com' } + let(:service) { 443 } + let(:family) { :INET } + let(:socktype) { :SOCK_STREAM } + let(:protocol) { 0 } + let(:flags) { Socket::AI_ALL | Socket::AI_V4MAPPED } + + let(:addrinfo_ipv4) do + double(Addrinfo, ipv4?: true, ipv6?: false, ip_address: '127.0.0.1') + end + let(:addrinfo_ipv6) do + double(Addrinfo, ipv4?: false, ipv6?: true, ip_address: '::1') + end + let(:addrinfo_list) { [addrinfo_ipv4, addrinfo_ipv6] } + + subject do + HostResolver.new( + service: service, + family: family, + socktype: socktype, + protocol: protocol, + flags: flags + ) + end + + describe '#initialize' do + it 'sets empty defaults' do + host_resolver = HostResolver.new + expect(host_resolver.service).to eq(443) + expect(host_resolver.family).to be_nil + expect(host_resolver.socktype).to eq(:SOCK_STREAM) + expect(host_resolver.protocol).to be_nil + expect(host_resolver.flags).to be_nil + end + end + + describe '#resolve_address' do + it 'uses instance defaults' do + expect(Addrinfo).to receive(:getaddrinfo).with( + nodename, + service, + family, + socktype, + protocol, + flags + ).and_return(addrinfo_list) + subject.resolve_address(nodename: nodename) + end + + it 'uses passed in options' do + service = 80 + family = :INET6 + socktype = :SOCK_DGRAM + protocol = nil + flags = nil + + expect(Addrinfo).to receive(:getaddrinfo).with( + nodename, + service, + family, + socktype, + protocol, + flags + ).and_return(addrinfo_list) + subject.resolve_address( + nodename: nodename, + service: service, + family: family, + socktype: socktype, + protocol: protocol, + flags: flags + ) + end + + it 'returns host address objects' do + expect(Addrinfo).to receive(:getaddrinfo).and_return(addrinfo_list) + _ipv6, ipv4 = subject.resolve_address(nodename: nodename) + expect(ipv4).to be_a(Hearth::DNS::HostAddress) + end + + context 'ipv6 is not available' do + before do + allow(subject).to receive(:use_ipv6?).and_return(false) + end + + it 'returns host addresses' do + expect(Addrinfo).to receive(:getaddrinfo).and_return(addrinfo_list) + ipv6, ipv4 = subject.resolve_address(nodename: nodename) + expect(ipv6).to be_nil + expect(ipv4.address).to eq(addrinfo_ipv4.ip_address) + end + end + + context 'ipv6 is available' do + before do + allow(subject).to receive(:use_ipv6?).and_return(true) + end + + it 'returns host addresses' do + expect(Addrinfo).to receive(:getaddrinfo).and_return(addrinfo_list) + ipv6, ipv4 = subject.resolve_address(nodename: nodename) + expect(ipv6.address).to eq(addrinfo_ipv6.ip_address) + expect(ipv4.address).to eq(addrinfo_ipv4.ip_address) + end + end + end + end + end +end diff --git a/hearth/spec/hearth/dns_spec.rb b/hearth/spec/hearth/dns_spec.rb new file mode 100644 index 000000000..f4c12b608 --- /dev/null +++ b/hearth/spec/hearth/dns_spec.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +module Hearth + module DNS + context 'DNS resolution patch' do + let(:host_resolver) { Hearth::DNS::HostResolver.new } + let(:hostname) { 'example.com' } + + let(:ipv6_host_address) do + Hearth::DNS::HostAddress.new( + address: '::1', + address_type: :AAAA, + hostname: hostname + ) + end + let(:ipv4_host_address) do + Hearth::DNS::HostAddress.new( + address: '127.0.0.1', + address_type: :A, + hostname: hostname + ) + end + + context 'IPSocket' do + context 'configured resolver' do + before do + Thread.current[:net_http_hearth_dns_resolver] = host_resolver + end + + it 'prefers ipv6' do + expect(host_resolver).to receive(:resolve_address) + .with(nodename: 'example.com') + .and_return([ipv6_host_address, ipv4_host_address]) + addr = IPSocket.getaddress('example.com') + expect(addr).to eq(ipv6_host_address.address) + end + + it 'falls back to ipv4' do + expect(host_resolver).to receive(:resolve_address) + .with(nodename: 'example.com') + .and_return([nil, ipv4_host_address]) + addr = IPSocket.getaddress('example.com') + expect(addr).to eq(ipv4_host_address.address) + end + end + + context 'no configured resolver' do + before do + Thread.current[:net_http_hearth_dns_resolver] = nil + end + + it 'uses stdlib if no resolver is set' do + expect(IPSocket).to receive(:original_hearth_getaddress) + .with(hostname) # aliased original method + expect(host_resolver).not_to receive(:resolve_address) + IPSocket.getaddress('example.com') + end + end + end + + context 'TCPSocket' do + let(:service) { 443 } + let(:proxy_host) { 'proxy.example.com' } + let(:proxy_addr) { '123.123.123.123' } + let(:proxy_port) { 8080 } + + context 'configured resolver' do + before do + Thread.current[:net_http_hearth_dns_resolver] = host_resolver + end + + it 'calls original with patched IPSocket' do + address = ipv4_host_address.address + expect(IPSocket).to receive(:getaddress) + .with(hostname).and_return(address) + expect_any_instance_of(TCPSocket) + .to receive(:original_hearth_initialize).with(address, service) + TCPSocket.new(hostname, service) + end + + it 'calls original with patched IPSocket and proxy' do + address = ipv4_host_address.address + expect(IPSocket).to receive(:getaddress) + .with(hostname).and_return(address) + expect(IPSocket).to receive(:getaddress) + .with(proxy_host).and_return(proxy_addr) + expect_any_instance_of(TCPSocket) + .to receive(:original_hearth_initialize) + .with(address, service, proxy_addr, proxy_port) + TCPSocket.new(hostname, service, proxy_host, proxy_port) + end + end + + context 'no configured resolver' do + before do + Thread.current[:net_http_hearth_dns_resolver] = nil + end + + it 'uses stdlib if no resolver is set' do + expect_any_instance_of(TCPSocket) + .to receive(:original_hearth_initialize) + .with(hostname, service, proxy_host, proxy_port) + expect(IPSocket).to_not receive(:getaddress) + TCPSocket.new(hostname, service, proxy_host, proxy_port) + end + end + end + end + end +end diff --git a/hearth/spec/hearth/http/api_error_spec.rb b/hearth/spec/hearth/http/api_error_spec.rb index b94a1e8b0..07ddd2efc 100644 --- a/hearth/spec/hearth/http/api_error_spec.rb +++ b/hearth/spec/hearth/http/api_error_spec.rb @@ -4,12 +4,12 @@ module Hearth module HTTP describe ApiError do let(:http_status) { 404 } - let(:http_headers) { Headers.new } + let(:http_fields) { Fields.new } let(:http_body) { 'body' } let(:http_resp) do Response.new( status: http_status, - headers: http_headers, + fields: http_fields, body: http_body ) end @@ -34,9 +34,9 @@ module HTTP end end - describe '#http_headers' do - it 'returns the http headers' do - expect(subject.http_headers).to eq(http_headers) + describe '#http_fields' do + it 'returns the http fields' do + expect(subject.http_fields).to eq(http_fields) end end diff --git a/hearth/spec/hearth/http/client_spec.rb b/hearth/spec/hearth/http/client_spec.rb index 49acd30c3..af31667dd 100644 --- a/hearth/spec/hearth/http/client_spec.rb +++ b/hearth/spec/hearth/http/client_spec.rb @@ -6,53 +6,65 @@ module Hearth module HTTP describe Client do before { WebMock.disable_net_connect! } + before do + ConnectionPool.pools.each(&:empty!) + end - let(:wire_trace) { false } + let(:debug_output) { false } let(:logger) { double('logger') } - let(:http_proxy) { nil } - let(:ssl_verify_peer) { true } - let(:ssl_ca_bundle) { nil } - let(:ssl_ca_directory) { nil } - let(:ssl_ca_store) { nil } + let(:proxy) { nil } + let(:ssl_timeout) { nil } + let(:verify_peer) { false } + let(:ca_file) { nil } + let(:ca_path) { nil } + let(:cert_store) { nil } + let(:host_resolver) { nil } subject do Client.new( - http_wire_trace: wire_trace, logger: logger, - http_proxy: http_proxy, - ssl_verify_peer: ssl_verify_peer, - ssl_ca_bundle: ssl_ca_bundle, - ssl_ca_directory: ssl_ca_directory, - ssl_ca_store: ssl_ca_store + logger: logger, + debug_output: debug_output, + proxy: proxy, + read_timeout: 1, + open_timeout: 1, + write_timeout: 1, + keep_alive_timeout: 1, + continue_timeout: 1, + ssl_timeout: ssl_timeout, + verify_peer: verify_peer, + ca_file: ca_file, + ca_path: ca_path, + cert_store: cert_store, + host_resolver: host_resolver ) end let(:http_method) { :get } - let(:url) { 'http://example.com' } - let(:headers) { {} } + let(:uri) { URI('http://example.com') } + let(:fields) { Fields.new } let(:request_body) { StringIO.new('') } let(:request) do Request.new( http_method: http_method, - url: url, - headers: headers, + uri: uri, + fields: fields, body: request_body ) end - let(:response) { Response.new } describe '#transmit' do - it 'sends the request to the url' do - stub_request(:any, url) + it 'sends the request to the uri' do + stub_request(:any, uri.to_s) subject.transmit(request: request, response: response) end %i[get post put patch delete].each do |http_method| it "sends a #{http_method} request" do - request = Request.new(http_method: http_method, url: url) + request = Request.new(http_method: http_method, uri: uri) - stub_request(http_method, url) + stub_request(http_method, uri.to_s) subject.transmit(request: request, response: response) end end @@ -65,7 +77,7 @@ module HTTP # webmock sets to nil expect_any_instance_of(Net::HTTP::Get) .to receive(:body_stream=).with(nil).and_call_original - stub_request(http_method, url) + stub_request(http_method, uri.to_s) .with(body: 'TEST_STRING') subject.transmit(request: request, response: response) end @@ -75,7 +87,7 @@ module HTTP it 'does not set the body stream' do expect_any_instance_of(Net::HTTP::Get) .to_not receive(:body_stream=) - stub_request(http_method, url) + stub_request(http_method, uri.to_s) subject.transmit(request: request, response: response) end end @@ -88,8 +100,7 @@ module HTTP wr.close request = Request.new( http_method: http_method, - url: url, - headers: headers, + uri: uri, body: rd ) # webmock sets to nil @@ -97,22 +108,33 @@ module HTTP .to receive(:body_stream=).with(nil).and_call_original expect_any_instance_of(Net::HTTP::Get) .to receive(:body_stream=).with(rd).and_call_original - stub_request(http_method, url) + stub_request(http_method, uri.to_s) subject.transmit(request: request, response: response) end end context 'request headers are set' do - let(:headers) { { 'Header-Name' => 'Header-Value' } } + before { request.headers['Header-Name'] = 'Header-Value' } + it 'transmits the headers' do - stub_request(http_method, url) - .with(headers: headers) + stub_request(http_method, uri.to_s) + .with(headers: request.headers.to_h) subject.transmit(request: request, response: response) end end + context 'request trailers are set' do + before { request.trailers['Trailer-Name'] = 'Trailer-Value' } + + it 'raises NotImplementedError' do + expect do + subject.transmit(request: request, response: response) + end.to raise_error(NotImplementedError) + end + end + it 'sets the response status code' do - stub_request(http_method, url) + stub_request(http_method, uri.to_s) .to_return(status: 242) subject.transmit(request: request, response: response) expect(response.status).to eq(242) @@ -123,15 +145,15 @@ module HTTP 'test-header' => 'value', 'test-header-2' => 'value2' } - stub_request(http_method, url) + stub_request(http_method, uri.to_s) .to_return(headers: response_headers) subject.transmit(request: request, response: response) - expect(response.headers).to eq(response_headers) + expect(response.headers.to_h).to eq(response_headers) end it 'writes the response body' do response_body = 'TEST-BODY' - stub_request(http_method, url) + stub_request(http_method, uri.to_s) .to_return(body: response_body) expect(response.body).to receive(:write).with(response_body) @@ -139,7 +161,7 @@ module HTTP end it 'rewinds the body' do - stub_request(http_method, url) + stub_request(http_method, uri.to_s) expect(response.body).to receive(:rewind) subject.transmit(request: request, response: response) @@ -147,23 +169,35 @@ module HTTP it 'raises ArgumentError on invalid http verbs' do expect do - request = Request.new(http_method: :invalid_verb, url: url) + request = Request.new(http_method: :invalid_verb, uri: uri) subject.transmit(request: request, response: response) end.to raise_error(ArgumentError) end - it 'rescues StandardError and converts to a NetworkingError' do - stub_request(:any, url).to_raise(StandardError) - expect do - subject.transmit(request: request, response: response) - end.to raise_error(NetworkingError) + it 'rescues StandardError and returns an HTTP::NetworkingError' do + stub_request(:any, uri.to_s).to_raise(StandardError) + resp_or_error = subject.transmit(request: request, response: response) + expect(resp_or_error).to be_a(NetworkingError) + end + + it 'configures timeouts' do + stub_request(:any, uri.to_s) + expect_any_instance_of(Net::HTTP).to receive(:start) do |http| + expect(http.open_timeout).to eq(1) + expect(http.read_timeout).to eq(1) + expect(http.write_timeout).to eq(1) + expect(http.continue_timeout).to eq(1) + expect(http.keep_alive_timeout).to eq(1) + end + + subject.transmit(request: request, response: response) end context 'https' do - let(:url) { 'https://example.com' } + let(:uri) { URI('https://example.com') } it 'sets use_ssl' do - stub_request(:any, url) + stub_request(:any, uri.to_s) expect_any_instance_of(Net::HTTP).to receive(:start) do |http| expect(http.use_ssl?).to be true http @@ -172,11 +206,11 @@ module HTTP subject.transmit(request: request, response: response) end - context 'ssl_verify_peer: false' do - let(:ssl_verify_peer) { false } + context 'verify_peer: false' do + let(:verify_peer) { false } it 'sets verify_peer to NONE' do - stub_request(:any, url) + stub_request(:any, uri.to_s) expect_any_instance_of(Net::HTTP).to receive(:start) do |http| expect(http.verify_mode).to eq OpenSSL::SSL::VERIFY_NONE http @@ -186,11 +220,11 @@ module HTTP end end - context 'ssl_verify_peer: true' do - let(:ssl_verify_peer) { true } + context 'verify_peer: true' do + let(:verify_peer) { true } it 'sets verify_peer to VERIFY_PEER' do - stub_request(:any, url) + stub_request(:any, uri.to_s) expect_any_instance_of(Net::HTTP).to receive(:start) do |http| expect(http.verify_mode).to eq OpenSSL::SSL::VERIFY_PEER http @@ -199,11 +233,24 @@ module HTTP subject.transmit(request: request, response: response) end - context 'ssl_ca_bundle' do - let(:ssl_ca_bundle) { 'ca_bundle' } + context 'ssl_timeout' do + let(:ssl_timeout) { 1 } + + it 'sets ssl_timeout' do + stub_request(:any, uri.to_s) + expect_any_instance_of(Net::HTTP).to receive(:start) do |http| + expect(http.ssl_timeout).to eq 1 + end + + subject.transmit(request: request, response: response) + end + end + + context 'ca_file' do + let(:ca_file) { 'ca_bundle' } it 'sets ca_file' do - stub_request(:any, url) + stub_request(:any, uri.to_s) expect_any_instance_of(Net::HTTP).to receive(:start) do |http| expect(http.ca_file).to eq 'ca_bundle' http @@ -213,11 +260,11 @@ module HTTP end end - context 'ssl_ca_directory' do - let(:ssl_ca_directory) { 'ca_directory' } + context 'ca_path' do + let(:ca_path) { 'ca_directory' } it 'sets ca_path' do - stub_request(:any, url) + stub_request(:any, uri.to_s) expect_any_instance_of(Net::HTTP).to receive(:start) do |http| expect(http.ca_path).to eq 'ca_directory' http @@ -227,13 +274,13 @@ module HTTP end end - context 'ssl_ca_store' do - let(:ssl_ca_store) { 'ca_store' } + context 'cert_store' do + let(:cert_store) { 'cert_store' } it 'sets cert_store' do - stub_request(:any, url) + stub_request(:any, uri.to_s) expect_any_instance_of(Net::HTTP).to receive(:start) do |http| - expect(http.cert_store).to eq 'ca_store' + expect(http.cert_store).to eq 'cert_store' http end @@ -243,10 +290,10 @@ module HTTP end end - context 'http_proxy set' do - let(:http_proxy) { 'http://my-proxy-host.com:88' } + context 'proxy set' do + let(:proxy) { 'http://my-proxy-host.com:88' } it 'sets the http proxy' do - stub_request(:any, url) + stub_request(:any, uri.to_s) expect_any_instance_of(Net::HTTP).to receive(:start) do |http| expect(http.proxyaddr).to eq('my-proxy-host.com') expect(http.proxyport).to eq(88) @@ -258,12 +305,12 @@ module HTTP context 'user and password set on proxy' do let(:password) { 'pass/word' } let(:user) { 'my user' } - let(:http_proxy) do + let(:proxy) do "http://#{CGI.escape(user)}:#{CGI.escape(password)}@my-proxy-host.com:88" end it 'unescapes and sets user and password' do - stub_request(:any, url) + stub_request(:any, uri.to_s) expect_any_instance_of(Net::HTTP).to receive(:start) do |http| expect(http.proxyaddr).to eq('my-proxy-host.com') expect(http.proxy_user).to eq(user) @@ -275,15 +322,112 @@ module HTTP end end - context 'http_wire_trace: true' do - let(:wire_trace) { true } + context 'debug_output: true' do + let(:debug_output) { true } + let(:request_logger) { double('request_logger') } it 'sets the logger on debug_output' do - stub_request(:any, url) + stub_request(:any, uri.to_s) expect_any_instance_of(Net::HTTP) .to receive(:set_debug_output).with(logger) + subject.transmit( + request: request, + response: response + ) + end + + it 'allows logger per request' do + stub_request(:any, uri.to_s) + expect_any_instance_of(Net::HTTP) + .to receive(:set_debug_output).with(request_logger) + subject.transmit( + request: request, + response: response, + logger: request_logger + ) + end + end + + context 'DNS resolution' do + let(:host_resolver) { Hearth::DNS::HostResolver.new } + + it 'sets the custom dns resolver as a thread local variable' do + expect(Thread.current).to receive(:[]=) + .with(:net_http_hearth_dns_resolver, host_resolver) + expect(Thread.current).to receive(:[]=) + .with(:net_http_hearth_dns_resolver, nil) + stub_request(:any, uri.to_s) + subject.transmit(request: request, response: response) + end + end + + context 'connection pooling' do + it 'gets a connection from the pool' do + stub_request(http_method, uri.to_s) + expect(ConnectionPool).to receive(:for).and_call_original + expect_any_instance_of(ConnectionPool).to receive(:connection_for) + .and_call_original subject.transmit(request: request, response: response) end + + it 'offers the connection back to the pool' do + stub_request(http_method, uri.to_s) + expect_any_instance_of(ConnectionPool).to receive(:offer) + .with(uri, an_instance_of(Hearth::HTTP::Client::HTTP)) + .and_call_original + subject.transmit(request: request, response: response) + end + + it 'finishes the connection if there is a networking error' do + stub_request(http_method, uri.to_s) + original_error = StandardError.new('failed') + error = Hearth::HTTP::NetworkingError.new(original_error) + expect_any_instance_of(Net::HTTP) + .to receive(:start).and_raise(original_error) + resp = subject.transmit(request: request, response: response) + expect(resp).to eq(error) + end + end + end + + describe HTTP do + let(:http) { Hearth::HTTP::Client::HTTP.new(net_http) } + let(:net_http) { Net::HTTP.new(request.uri.host, request.uri.port) } + + it 'delegates to Net::HTTP' do + expect(http).to be_a(Delegator) + expect(http.__getobj__).to be(net_http) + end + + describe '#stale?' do + let(:base_time_ms) { 0 } + let(:fresh_time_ms) { 1000 } + let(:stale_time_ms) { 3000 } + + before do + net_http.keep_alive_timeout = 2 + allow(net_http).to receive(:request) + end + + it 'uses last used time to determine staleness' do + expect(Process).to receive(:clock_gettime).and_return(base_time_ms) + http.request(request) + expect(Process).to receive(:clock_gettime).and_return(fresh_time_ms) + expect(http.stale?).to be(false) + expect(Process).to receive(:clock_gettime).and_return(stale_time_ms) + expect(http.stale?).to be(true) + end + + it 'is stale if not used' do + expect(http.stale?).to be(true) + end + end + + describe '#finish' do + it 'closes the connection without errors' do + expect(net_http).to receive(:finish).and_raise(IOError) + expect { http.finish }.not_to raise_error + end end end end diff --git a/hearth/spec/hearth/retry/error_inspector_spec.rb b/hearth/spec/hearth/http/error_inspector_spec.rb similarity index 68% rename from hearth/spec/hearth/retry/error_inspector_spec.rb rename to hearth/spec/hearth/http/error_inspector_spec.rb index d53854e90..8830555e3 100644 --- a/hearth/spec/hearth/retry/error_inspector_spec.rb +++ b/hearth/spec/hearth/http/error_inspector_spec.rb @@ -1,19 +1,13 @@ # frozen_string_literal: true module Hearth - module Retry + module HTTP describe ErrorInspector do - subject { ErrorInspector.new(error, http_status) } + subject { ErrorInspector.new(error, http_resp) } let(:http_status) { 404 } - let(:http_headers) { Hearth::HTTP::Headers.new } - let(:http_body) { 'body' } let(:http_resp) do - Hearth::HTTP::Response.new( - status: http_status, - headers: http_headers, - body: http_body - ) + Hearth::HTTP::Response.new(status: http_status) end let(:message) { 'message' } @@ -66,7 +60,7 @@ module Retry end end - context 'error is networking' do + context 'error is transient' do let(:error) { Hearth::HTTP::NetworkingError.new(StandardError.new) } it 'returns true' do @@ -76,7 +70,7 @@ module Retry end describe '#error_type' do - context 'networking error' do + context 'transient error' do let(:error) { Hearth::HTTP::NetworkingError.new(StandardError.new) } it 'returns Transient' do @@ -129,6 +123,42 @@ module Retry end end end + + describe '#hints' do + context 'retry_after_hint' do + context 'header is an integer' do + it 'hint returns an integer delay' do + http_resp.headers['retry-after'] = '123' + expect(subject.hints[:retry_after_hint]).to eq(123) + end + end + + context 'header is a date' do + let(:time) { Time.new(2023, 1, 24) } + let(:retry_after_time) { (time + 123).httpdate } + + it 'hint returns an integer delay' do + http_resp.headers['retry-after'] = retry_after_time + allow(Time).to receive(:now).and_return(time) + expect(subject.hints[:retry_after_hint]).to eq(123) + end + end + + context 'header is nil' do + it 'no hint' do + http_resp.headers['retry-after'] = nil + expect(subject.hints.key?(:retry_after_hint)).to eq(false) + end + end + + context 'header is an empty string' do + it 'no hint' do + http_resp.headers['retry-after'] = '' + expect(subject.hints.key?(:retry_after_hint)).to eq(false) + end + end + end + end end end end diff --git a/hearth/spec/hearth/http/error_parser_spec.rb b/hearth/spec/hearth/http/error_parser_spec.rb index b1d9f27c0..6ba65c952 100644 --- a/hearth/spec/hearth/http/error_parser_spec.rb +++ b/hearth/spec/hearth/http/error_parser_spec.rb @@ -14,6 +14,8 @@ def initialize(location:, **kwargs) @location = location super(**kwargs) end + + attr_reader :location end class ApiClientError < ApiError; end @@ -29,7 +31,8 @@ class TestModeledError < ApiClientError; end let(:errors) { [TestErrors::TestModeledError] } let(:resp_status) { 200 } - let(:http_resp) { Response.new(status: resp_status) } + let(:fields) { Fields.new } + let(:http_resp) { Response.new(status: resp_status, fields: fields) } let(:metadata) { { key: 'value' } } subject do @@ -60,11 +63,19 @@ class TestModeledError < ApiClientError; end end context 'error response: 3XX code' do + let(:field) { Field.new('Location', 'http://example.com') } + let(:fields) { Fields.new([field]) } + let(:resp_status) { 300 } it 'returns an APIRedirectError' do error = subject.parse(http_resp, metadata) expect(error).to be_a(TestErrors::ApiRedirectError) end + + it 'populates a location' do + error = subject.parse(http_resp, metadata) + expect(error.location).to eq('http://example.com') + end end context 'error response: 4XX code' do diff --git a/hearth/spec/hearth/http/field_spec.rb b/hearth/spec/hearth/http/field_spec.rb new file mode 100644 index 000000000..a0563794b --- /dev/null +++ b/hearth/spec/hearth/http/field_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +module Hearth + module HTTP + describe Field do + let(:header) { Field.new('X-Header', 'foo') } + let(:trailer) { Field.new('X-Trailer', 'bar', kind: :trailer) } + + describe '#initialize' do + it 'raises when name is nil' do + expect { Field.new(nil) }.to raise_error(ArgumentError) + end + + it 'raises when name is empty' do + expect { Field.new('') }.to raise_error(ArgumentError) + end + + it 'defaults to header kind' do + expect(Field.new('header').kind).to eq(:header) + end + end + + it 'is immutable' do + expect { header.name = 'X-Header-2' }.to raise_error(NoMethodError) + expect { header.value = 'bar' }.to raise_error(NoMethodError) + expect { header.kind = :trailer }.to raise_error(NoMethodError) + end + + describe '#value' do + let(:time) { Time.now } + + context 'value is a Scalar type' do + let(:header_int) { Field.new('X-HeaderInt', 42) } + let(:header_float) { Field.new('X-HeaderFloat', 420.69) } + let(:header_time) { Field.new('X-HeaderTime', time) } + + it 'returns the value as a String' do + expect(header.value).to eq('foo') + expect(header_int.value).to eq('42') + expect(header_float.value).to eq('420.69') + expect(header_time.value).to eq(time.to_s) + end + end + + context 'value is an Array' do + let(:header_list_scalar) do + Field.new('X-HeaderList', ['foo', 42, 420.69, time]) + end + let(:header_list_escape) do + Field.new('X-HeaderList', ['bar, baz', '"quoted"']) + end + + it 'returns the value as a String' do + expect(header_list_scalar.value) + .to eq("foo, 42, 420.69, #{time}") + expect(header_list_escape.value) + .to eq('"bar, baz", "\"quoted\""') + end + end + + context 'encoding' do + it 'allows for different encoding' do + expect(header.value('UTF-16').encoding).to eq(Encoding::UTF_16) + end + end + end + + describe '#header?' do + it 'returns true when kind is :header' do + expect(header.header?).to eq(true) + expect(trailer.header?).to eq(false) + end + end + + describe '#trailer?' do + it 'returns true when kind is :trailer' do + expect(header.trailer?).to eq(false) + expect(trailer.trailer?).to eq(true) + end + end + + describe '#to_h' do + it 'returns a hash with the field name as key and value as value' do + # ensure value method is called + expect(header).to receive(:value).and_call_original + expect(header.to_h).to eq('X-Header' => 'foo') + end + end + end + end +end diff --git a/hearth/spec/hearth/http/fields_spec.rb b/hearth/spec/hearth/http/fields_spec.rb new file mode 100644 index 000000000..e51f876bc --- /dev/null +++ b/hearth/spec/hearth/http/fields_spec.rb @@ -0,0 +1,175 @@ +# frozen_string_literal: true + +module Hearth + module HTTP + describe Fields do + let(:header_field) { Field.new('X-Header', 'foo') } + let(:trailer_field) { Field.new('X-Trailer', 'bar', kind: :trailer) } + + let(:fields) { Fields.new([header_field, trailer_field]) } + + describe '#initialize' do + it 'defaults encoding to UTF-8' do + expect(Fields.new.encoding).to eq('utf-8') + end + + it 'raises when fields is not an Array' do + expect { Fields.new(nil) }.to raise_error(ArgumentError) + expect { Fields.new('not an array') }.to raise_error(ArgumentError) + end + + it 'defaults to empty' do + expect(Fields.new.size).to eq(0) + end + + it 'initializes with fields' do + expect(Fields.new([header_field]).size).to eq(1) + end + end + + describe '#[]' do + it 'returns the field' do + expect(fields['x-header']).to eq(header_field) + end + end + + describe '#[]=' do + it 'raises when field is not a Field' do + expect { fields['x-header'] = 'not a field' } + .to raise_error(ArgumentError) + end + + it 'sets the field' do + fields['x-header'] = Field.new('X-Header', 'bar') + expect(fields['x-header'].value).to eq('bar') + end + end + + describe '#key?' do + it 'returns true if the field exists' do + expect(fields.key?('x-header')).to eq(true) + expect(fields.key?('x-trailer')).to eq(true) + expect(fields.key?('x-foo')).to eq(false) + end + end + + describe '#delete' do + it 'deletes the field' do + fields.delete('x-header') + expect(fields.key?('x-header')).to eq(false) + end + + it 'returns the deleted field' do + field = fields['x-header'] + deleted = fields.delete('x-header') + expect(deleted).to eq(field) + end + end + + describe '#each' do + it 'includes Enumerable' do + expect(fields).to be_a(Enumerable) + end + + it 'enumerates over its contents' do + fields.each { |k, v| expect(fields[k]).to eq(v) } + end + end + + describe '#size' do + it 'returns the number of fields' do + expect(fields.size).to eq(2) + end + end + + describe '#clear' do + it 'clears the fields' do + fields.clear + expect(fields.size).to eq(0) + end + end + + describe Fields::Proxy do + let(:proxy) { Fields::Proxy.new(fields, :header) } + + describe '#[]' do + it 'returns the field value' do + expect(proxy['x-header']).to eq(header_field.value) + end + + it 'returns nil if the kind of field does not exist' do + expect(proxy['x-trailer']).to be_nil + end + + context 'encoding' do + let(:fields) { Fields.new([header_field], encoding: 'UTF-16') } + + it 'applies the encoding' do + expect(proxy['x-header'].encoding).to eq(Encoding::UTF_16) + end + end + end + + describe '#[]=' do + it 'sets the field value and kind' do + proxy['x-foo'] = 'bar' + expect(fields['x-foo']).to be_a(Field) + expect(fields['x-foo'].value).to eq('bar') + expect(fields['x-foo'].kind).to eq(:header) + end + end + + describe '#key?' do + it 'returns true if the field of that kind exists' do + expect(proxy.key?('x-header')).to eq(true) + expect(proxy.key?('x-trailer')).to eq(false) + end + end + + describe '#delete' do + it 'deletes the kind of field' do + proxy.delete('x-header') + expect(fields.key?('x-header')).to eq(false) + proxy.delete('x-trailer') + expect(fields.key?('x-trailer')).to eq(true) + end + + it 'returns the value of the deleted field' do + header = proxy['x-header'] + deleted = proxy.delete('x-header') + expect(deleted).to eq(header) + end + + context 'encoding' do + let(:fields) { Fields.new([header_field], encoding: 'UTF-16') } + + it 'applies the encoding' do + expect(proxy.delete('x-header').encoding) + .to eq(Encoding::UTF_16) + end + end + end + + describe '#each' do + it 'includes Enumerable' do + expect(proxy).to be_a(Enumerable) + end + + it 'returns the Field name and value for each field kind' do + # not downcased header name + expect(proxy.each.to_h).to eq('X-Header' => 'foo') + end + + context 'encoding' do + let(:fields) { Fields.new([header_field], encoding: 'UTF-16') } + + it 'applies the encoding' do + expect(proxy.each.to_h['X-Header'].encoding) + .to eq(Encoding::UTF_16) + end + end + end + end + end + end +end diff --git a/hearth/spec/hearth/http/headers_spec.rb b/hearth/spec/hearth/http/headers_spec.rb deleted file mode 100644 index 1447088e5..000000000 --- a/hearth/spec/hearth/http/headers_spec.rb +++ /dev/null @@ -1,95 +0,0 @@ -# frozen_string_literal: true - -module Hearth - module HTTP - describe Headers do - let(:header1) { 'test-header' } - let(:header1_normalized) { 'Test-Header' } - let(:value1) { 'test header value' } - - let(:header2) { 'X-thing-Mixed' } - let(:header2_normalized) { 'X-Thing-Mixed' } - let(:value2) { 'mixed value' } - - let(:headers_hash) { { header1 => value1, header2 => value2 } } - - subject { Headers.new(headers_hash) } - - describe '#initialize' do - it 'sets and normalizes the headers' do - expect(subject.size).to eq(headers_hash.size) - expect(subject.keys) - .to include(header1_normalized, header2_normalized) - end - end - - describe '#[]' do - it 'normalizes the key' do - expect(subject[header1]).to eq(value1) - expect(subject[header1_normalized]).to eq(value1) - end - end - - describe '#[]=' do - let(:new_value) { 'new value' } - let(:integer_value) { 1 } - - it 'normalizes the key and sets the value' do - subject[header1] = new_value - expect(subject[header1_normalized]).to eq(new_value) - end - - it 'converts values to string' do - subject[header1] = integer_value - expect(subject[header1]).to eq(integer_value.to_s) - end - end - - describe '#key?' do - it 'normalizes the key' do - expect(subject.key?(header1)).to be(true) - end - - it 'returns false when the key does not exist' do - expect(subject.key?('not-found')).to be(false) - end - end - - describe '#delete' do - it 'deletes the normalized key' do - subject.delete(header1) - expect(subject.keys).not_to include(header1_normalized) - end - end - - describe '#each' do - it 'enumerates over its contents' do - subject.each { |k, v| expect(subject[k]).to eq(v) } - end - end - - describe '#size' do - it 'returns the size' do - expect(subject.size).to eq(headers_hash.size) - end - end - - describe '#update' do - it 'accepts a hash, updating self' do - subject.update(:abc => 123, 'xyz' => '234', header2 => 'new') - expect(subject['abc']).to eq('123') - expect(subject['xyz']).to eq('234') - expect(subject[header1]).to eq(value1) - expect(subject[header2]).to eq('new') - end - end - - describe '#clear' do - it 'clears the headers' do - subject.clear - expect(subject.size).to eq(0) - end - end - end - end -end diff --git a/hearth/spec/hearth/http/middleware/content_length_spec.rb b/hearth/spec/hearth/http/middleware/content_length_spec.rb index f807e2695..839231e1f 100644 --- a/hearth/spec/hearth/http/middleware/content_length_spec.rb +++ b/hearth/spec/hearth/http/middleware/content_length_spec.rb @@ -14,8 +14,7 @@ module Middleware let(:request) do Request.new( - http_method: :get, - url: 'http://example.com', + http_method: 'GET', body: body ) end @@ -47,7 +46,8 @@ module Middleware expect(app).to receive(:call).with(input, context) resp = subject.call(input, context) - expect(request.headers['Content-Length'].to_i).to eq(body.size) + expect(request.headers['Content-Length']) + .to eq(body.size.to_s) expect(resp).to be output end end diff --git a/hearth/spec/hearth/http/middleware/content_md5_spec.rb b/hearth/spec/hearth/http/middleware/content_md5_spec.rb index 6b8700df1..23173a48b 100644 --- a/hearth/spec/hearth/http/middleware/content_md5_spec.rb +++ b/hearth/spec/hearth/http/middleware/content_md5_spec.rb @@ -3,7 +3,7 @@ module Hearth module HTTP module Middleware - describe ContentLength do + describe ContentMD5 do let(:app) { double('app', call: output) } subject { ContentMD5.new(app) } @@ -16,8 +16,7 @@ module Middleware let(:request) do Request.new( - http_method: :get, - url: 'http://example.com', + http_method: 'GET', body: body ) end diff --git a/hearth/spec/hearth/http/networking_error_spec.rb b/hearth/spec/hearth/http/networking_error_spec.rb deleted file mode 100644 index 5b47efa68..000000000 --- a/hearth/spec/hearth/http/networking_error_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -module Hearth - module HTTP - describe NetworkingError do - let(:original_message) { 'ORIG-MESSAGE' } - let(:original_error) { StandardError.new(original_message) } - - subject { NetworkingError.new(original_error) } - - it 'subclasses StandardError' do - expect(subject).to be_a StandardError - end - - it 'adds to the original errors message' do - expect(subject.message).to include(original_message) - end - end - end -end diff --git a/hearth/spec/hearth/http/request_spec.rb b/hearth/spec/hearth/http/request_spec.rb index b484bc3ea..f88afbcc0 100644 --- a/hearth/spec/hearth/http/request_spec.rb +++ b/hearth/spec/hearth/http/request_spec.rb @@ -4,15 +4,15 @@ module Hearth module HTTP describe Request do let(:http_method) { :get } - let(:url) { 'http://example.com' } - let(:headers) { Headers.new({ 'key' => 'value' }) } + let(:uri) { URI('http://example.com') } + let(:fields) { Fields.new } let(:body) { 'body' } subject do Request.new( http_method: http_method, - url: url, - headers: headers, + uri: uri, + fields: fields, body: body ) end @@ -21,82 +21,109 @@ module HTTP it 'sets empty defaults' do request = Request.new expect(request.http_method).to be_nil - expect(request.url).to be_nil - expect(request.headers).to be_a Headers - expect(request.body).to be_a StringIO + expect(request.fields).to be_a(Fields) + expect(request.body).to be_a(StringIO) + expect(request.uri).to be_a(URI) + end + end + + describe '#headers' do + it 'allows setting of headers' do + request = Request.new + request.headers['name'] = 'value' + expect(request.fields['name'].value).to eq('value') + expect(request.fields['name'].kind).to eq(:header) + end + + it 'lets you get a hash of only the headers' do + request = Request.new + request.headers['name'] = 'value' + request.trailers['trailer'] = 'trailer-value' + expect(request.headers.to_h).to eq('name' => 'value') + end + end + + describe '#trailers' do + it 'allows setting of trailers' do + request = Request.new + request.trailers['name'] = 'value' + expect(request.fields['name'].value).to eq('value') + expect(request.fields['name'].kind).to eq(:trailer) + end + + it 'lets you get a hash of only the trailers' do + request = Request.new + request.trailers['name'] = 'value' + request.headers['header'] = 'header-value' + expect(request.trailers.to_h).to eq('name' => 'value') end end describe '#append_path' do - it 'appends to the url' do + it 'appends to the uri' do subject.append_path('test') - expect(subject.url).to eq('http://example.com/test') + expect(subject.uri.to_s).to eq('http://example.com/test') end it 'removes trailing slash' do - subject.url += '/' + subject.uri += '/' subject.append_path('test') - expect(subject.url).to eq('http://example.com/test') + expect(subject.uri.to_s).to eq('http://example.com/test') end it 'removes prefix slash' do subject.append_path('/test') - expect(subject.url).to eq('http://example.com/test') + expect(subject.uri.to_s).to eq('http://example.com/test') end end describe '#append_query_param' do it 'appends a single value' do subject.append_query_param('test') - expect(subject.url).to eq('http://example.com?test') + expect(subject.uri.to_s).to eq('http://example.com?test') end it 'appends a pair of values' do subject.append_query_param('test', 'value') - expect(subject.url).to eq('http://example.com?test=value') + expect(subject.uri.to_s).to eq('http://example.com?test=value') end it 'raises an ArgumentError for invalid number of arguments' do - expect { subject.append_query_param('test', 'value', 'invlaid') } + expect { subject.append_query_param('test', 'value', 'invalid') } .to raise_error(ArgumentError) end it 'appends to existing query params' do subject.append_query_param('test', 'value') subject.append_query_param('test2') - expect(subject.url).to eq('http://example.com?test=value&test2') + expect(subject.uri.to_s).to eq('http://example.com?test=value&test2') end - it 'url escapes parameters and values' do + it 'uri escapes parameters and values' do subject.append_query_param('test space', 'test/value') - expect(subject.url) + expect(subject.uri.to_s) .to eq('http://example.com?test%20space=test%2Fvalue') end end - describe '#append_query_params' do + describe '#append_query_param_list' do it 'appends a param list' do - params = Hearth::Query::ParamList.new - params['key 1'] = nil - params['key 2'] = 'value 2' - subject.append_query_params(params) - expect(subject.url).to eq('http://example.com?key%201=&key%202=value%202') - end - - it 'appends to existing query params' do subject.append_query_param('original') params = Hearth::Query::ParamList.new params['key 1'] = nil - params['key 2'] = 'value 2' - subject.append_query_params(params) - expect(subject.url).to eq('http://example.com?original&key%201=&key%202=value%202') + params['key 2'] = '' + params['key 3'] = 'value' + params['key 4'] = %w[value value2] + subject.append_query_param_list(params) + expect(subject.uri.to_s) + .to eq('http://example.com?original&key%201&key%202=&key%203=value&key%204=value&key%204=value2') end end describe '#prefix_host' do it 'prefixes the host' do subject.prefix_host('data.') - expect(subject.url).to eq('http://data.example.com') + expect(subject.uri.to_s).to eq('http://data.example.com') end end end diff --git a/hearth/spec/hearth/http/response_spec.rb b/hearth/spec/hearth/http/response_spec.rb index aa459712e..7c6efc286 100644 --- a/hearth/spec/hearth/http/response_spec.rb +++ b/hearth/spec/hearth/http/response_spec.rb @@ -6,8 +6,9 @@ module HTTP describe '#initialize' do it 'sets empty defaults' do response = Response.new + expect(response.body).to be_a(StringIO) expect(response.status).to eq(0) - expect(response.headers).to be_a(Headers) + expect(response.fields).to be_a(Fields) expect(response.body).to be_a(StringIO) end end @@ -15,13 +16,16 @@ module HTTP describe '#reset' do it 'resets to defaults' do response = Response.new( + reason: 'Because', status: 200, - headers: Headers.new({ 'key' => 'value' }) + fields: Fields.new([Field.new('key', 'value')]) ) + response.headers['key'] = 'value' response.body << 'foo bar' # frozen string literal, cannot pass in response.reset expect(response.status).to eq(0) - expect(response.headers.size).to eq(0) + expect(response.fields.size).to eq(0) + expect(response.reason).to be_nil response.body.rewind # ensure nothing is there when we read expect(response.body.read).to eq('') end diff --git a/hearth/spec/hearth/middleware/host_prefix_spec.rb b/hearth/spec/hearth/middleware/host_prefix_spec.rb index b9bc2986d..aeda0e416 100644 --- a/hearth/spec/hearth/middleware/host_prefix_spec.rb +++ b/hearth/spec/hearth/middleware/host_prefix_spec.rb @@ -19,8 +19,8 @@ module Middleware let(:input) { struct.new } let(:output) { double('output') } - let(:url) { 'https://example.com' } - let(:request) { Hearth::HTTP::Request.new(url: url) } + let(:uri) { URI('https://example.com') } + let(:request) { Hearth::HTTP::Request.new(uri: uri) } let(:response) { double('response') } let(:context) do Context.new( @@ -38,7 +38,7 @@ module Middleware expect(app).to receive(:call).with(input, context).ordered resp = subject.call(input, context) - expect(request.url).to eq('https://foo.example.com') + expect(request.uri.to_s).to eq('https://foo.example.com') expect(resp).to be output end @@ -50,7 +50,7 @@ module Middleware expect(app).to receive(:call).with(input, context) resp = subject.call(input, context) - expect(request.url).to eq('https://bar.example.com') + expect(request.uri.to_s).to eq('https://bar.example.com') expect(resp).to be output end @@ -83,7 +83,7 @@ module Middleware expect(app).to receive(:call).with(input, context) resp = subject.call(input, context) - expect(request.url).to eq(url) + expect(request.uri).to eq(uri) expect(resp).to be output end end diff --git a/hearth/spec/hearth/middleware/retry_spec.rb b/hearth/spec/hearth/middleware/retry_spec.rb index 2a1d86122..b9e00b52c 100644 --- a/hearth/spec/hearth/middleware/retry_spec.rb +++ b/hearth/spec/hearth/middleware/retry_spec.rb @@ -31,12 +31,8 @@ def handle_with_retry(test_cases, middleware_args = {}) subject = Hearth::Middleware::Retry.new( app, - retry_mode: middleware_args[:retry_mode], - max_attempts: middleware_args[:max_attempts], - adaptive_retry_wait_to_fill: middleware_args[:adaptive_retry_wait_to_fill], - error_inspector_class: Hearth::Retry::ErrorInspector, - retry_quota: retry_quota, - client_rate_limiter: client_rate_limiter + error_inspector_class: Hearth::HTTP::ErrorInspector, + retry_strategy: middleware_args[:retry_strategy] ) subject.call(input, context) @@ -56,6 +52,11 @@ def apply_expectations(retry_class, test_case) # Don't actually sleep allow(Kernel).to receive(:sleep) + retry_strategy = retry_class.instance_variable_get(:@retry_strategy) + retry_quota = retry_strategy.instance_variable_get(:@retry_quota) + client_rate_limiter = + retry_strategy.instance_variable_get(:@client_rate_limiter) + if expected[:retries] expect(retry_class.instance_variable_get(:@retries)) .to eq(expected[:retries]) @@ -92,16 +93,15 @@ def setup_next_response(context, test_case) module Hearth module Middleware describe Retry do - let(:retry_quota) { Hearth::Retry::RetryQuota.new } - let(:client_rate_limiter) { Hearth::Retry::ClientRateLimiter.new } - let(:input) { double('Type::OperationInput') } + let(:error) do Hearth::ApiError.new( error_code: 'error_code', metadata: {} ) end + let(:request) { Hearth::HTTP::Request.new } let(:response) { Hearth::HTTP::Response.new } let(:context) do @@ -111,13 +111,16 @@ module Middleware ) end + before { allow(error).to receive(:retryable?).and_return(true) } + context 'standard mode' do + let(:retry_strategy) { Hearth::Retry::Standard.new } + let(:retry_quota) do + retry_strategy.instance_variable_get(:@retry_quota) + end + let(:middleware_args) do - { - retry_mode: 'standard', - max_attempts: 3, - adaptive_retry_wait_to_fill: true - } + { retry_strategy: retry_strategy } end before do @@ -216,11 +219,14 @@ module Middleware } ] - handle_with_retry(test_cases, middleware_args.merge(max_attempts: 5)) + args = middleware_args.merge( + retry_strategy: Hearth::Retry::Standard.new(max_attempts: 5) + ) + handle_with_retry(test_cases, args) end it 'does not exceed the max backoff time' do - stub_const('Hearth::Middleware::Retry::MAX_BACKOFF', 3) + stub_const('Hearth::Retry::ExponentialBackoff::MAX_BACKOFF', 3) test_cases = [ { @@ -245,17 +251,24 @@ module Middleware } ] - handle_with_retry(test_cases, middleware_args.merge(max_attempts: 5)) + args = middleware_args.merge( + retry_strategy: Hearth::Retry::Standard.new(max_attempts: 5) + ) + handle_with_retry(test_cases, args) end end context 'adaptive mode' do + let(:retry_strategy) { Hearth::Retry::Adaptive.new } + let(:retry_quota) do + retry_strategy.instance_variable_get(:@retry_quota) + end + let(:client_rate_limiter) do + retry_strategy.instance_variable_get(:@client_rate_limiter) + end + let(:middleware_args) do - { - retry_mode: 'adaptive', - max_attempts: 3, - adaptive_retry_wait_to_fill: true - } + { retry_strategy: retry_strategy } end it 'verifies cubic calculations for successes' do diff --git a/hearth/spec/hearth/middleware/send_spec.rb b/hearth/spec/hearth/middleware/send_spec.rb index b29fd2389..d2b5aaebf 100644 --- a/hearth/spec/hearth/middleware/send_spec.rb +++ b/hearth/spec/hearth/middleware/send_spec.rb @@ -13,6 +13,7 @@ module Middleware let(:stub_class) { double('stub_class') } let(:params_class) { double('params_class') } let(:stubs) { Hearth::Stubbing::Stubs.new } + let(:logger) { double('Logger') } subject do Send.new( @@ -30,24 +31,39 @@ module Middleware let(:input) { double('Type::OperationInput') } let(:request) { double('request') } - let(:response) { double('response') } + let(:body) { StringIO.new } + let(:response) { double('response', body: body) } let(:context) do Hearth::Context.new( request: request, response: response, - operation_name: operation + operation_name: operation, + logger: logger ) end it 'sends the request and returns an output object' do expect(client).to receive(:transmit).with( request: request, - response: response - ) + response: response, + logger: logger + ).and_return(response) - expect( - subject.call(input, context) - ).to be_a Hearth::Output + output = subject.call(input, context) + expect(output).to be_a(Hearth::Output) + end + + it 'sets output error to NetworkingError if the request fails' do + error = Hearth::HTTP::NetworkingError.new(StandardError.new) + expect(client).to receive(:transmit).with( + request: request, + response: response, + logger: logger + ).and_return(error) + + output = subject.call(input, context) + expect(output).to be_a(Hearth::Output) + expect(output.error).to be_a(Hearth::NetworkingError) end context 'stub_responses is true' do @@ -60,6 +76,13 @@ module Middleware expect(output.error).to be_a(Exception) end + it 'rewinds the body' do + expect(stubs).to receive(:next) + .with(:operation).and_return(Exception) + expect(body).to receive(:rewind) + subject.call(input, context) + end + context 'stub is a proc' do before { stubs.add_stubs(operation, [stub_proc]) } diff --git a/hearth/spec/hearth/networking_error_spec.rb b/hearth/spec/hearth/networking_error_spec.rb new file mode 100644 index 000000000..bee211a60 --- /dev/null +++ b/hearth/spec/hearth/networking_error_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Hearth + describe NetworkingError do + let(:original_message) { 'ORIG-MESSAGE' } + let(:original_error) { StandardError.new(original_message) } + + subject { NetworkingError.new(original_error) } + + it 'subclasses StandardError' do + expect(subject).to be_a StandardError + end + + it 'adds to the original errors message' do + expect(subject.message).to include(original_message) + end + end +end diff --git a/hearth/spec/hearth/query/param_spec.rb b/hearth/spec/hearth/query/param_spec.rb index 31ec5edd1..efb5913f8 100644 --- a/hearth/spec/hearth/query/param_spec.rb +++ b/hearth/spec/hearth/query/param_spec.rb @@ -27,12 +27,12 @@ module Query it 'leaves the trailing = when value is nil' do param = Param.new('key') - expect(param.to_s).to eq('key=') + expect(param.to_s).to eq('key') end it 'can handle arrays' do param = Param.new('foo', ['1', nil, '3']) - expect(param.to_s).to eq('foo=1&foo=&foo=3') + expect(param.to_s).to eq('foo=1&foo&foo=3') end end diff --git a/hearth/spec/hearth/request_spec.rb b/hearth/spec/hearth/request_spec.rb new file mode 100644 index 000000000..6493ee6f1 --- /dev/null +++ b/hearth/spec/hearth/request_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Hearth + describe Request do + let(:uri) { URI('http://example.com') } + let(:body) { 'body' } + + subject { Request.new(uri: uri, body: body) } + + describe '#initialize' do + it 'sets empty defaults' do + request = Request.new + expect(request.body).to be_a(StringIO) + expect(request.uri).to be_a(URI) + end + end + end +end diff --git a/hearth/spec/hearth/response_spec.rb b/hearth/spec/hearth/response_spec.rb new file mode 100644 index 000000000..dfee87380 --- /dev/null +++ b/hearth/spec/hearth/response_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Hearth + describe Response do + describe '#initialize' do + it 'sets empty defaults' do + response = Response.new + expect(response.body).to be_a(StringIO) + end + end + end +end diff --git a/hearth/spec/hearth/retry/strategy_spec.rb b/hearth/spec/hearth/retry/strategy_spec.rb new file mode 100644 index 000000000..887e9c24c --- /dev/null +++ b/hearth/spec/hearth/retry/strategy_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Hearth + module Retry + describe Strategy do + subject { Strategy.new } + + it 'defines the interface' do + expect { subject.acquire_initial_retry_token } + .to raise_error(NotImplementedError) + expect { subject.refresh_retry_token(nil, nil) } + .to raise_error(NotImplementedError) + expect { subject.record_success(nil) } + .to raise_error(NotImplementedError) + end + end + end +end diff --git a/hearth/spec/hearth/time_helper_spec.rb b/hearth/spec/hearth/time_helper_spec.rb index 306ea03a1..6ffcb3ec4 100644 --- a/hearth/spec/hearth/time_helper_spec.rb +++ b/hearth/spec/hearth/time_helper_spec.rb @@ -8,18 +8,40 @@ module Hearth it 'converts a time object to date time format' do expect(subject.to_date_time(time)).to eq '1970-01-01T00:00:00Z' end + + context 'fractional seconds' do + let(:time) { Time.at(946_845_296, 123, :millisecond) } + it 'converts to date time format with milliseconds' do + expect(subject.to_date_time(time)).to eq '2000-01-02T20:34:56.123Z' + end + end end describe '.to_epoch_seconds' do it 'converts a time object to epoch seconds format' do expect(subject.to_epoch_seconds(time)).to eq 0.0 end + + context 'fractional seconds' do + let(:time) { Time.at(946_845_296, 123, :millisecond) } + it 'converts to date time format with milliseconds' do + expect(subject.to_epoch_seconds(time)).to eq 946_845_296.123 + end + end end describe '.to_http_date' do it 'converts a time object to http date format' do expect(subject.to_http_date(time)).to eq 'Thu, 01 Jan 1970 00:00:00 GMT' end + + context 'fractional seconds' do + let(:time) { Time.at(946_845_296, 123, :millisecond) } + it 'converts to http date format with milliseconds' do + expect(subject.to_http_date(time)) + .to eq 'Sun, 02 Jan 2000 20:34:56.123 GMT' + end + end end end end diff --git a/sample-service/.ruby-version b/sample-service/.ruby-version new file mode 100644 index 000000000..818bd47ab --- /dev/null +++ b/sample-service/.ruby-version @@ -0,0 +1 @@ +3.0.6 diff --git a/sample-service/Gemfile b/sample-service/Gemfile index eebaa7247..c561435ed 100644 --- a/sample-service/Gemfile +++ b/sample-service/Gemfile @@ -1,7 +1,7 @@ source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } -ruby '3.0.2' +ruby '3.0.6' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails', branch: 'main' gem 'rails', '~> 6.1.4', '>= 6.1.4.1'