diff --git a/gems/smithy-client/lib/smithy-client/plugins/user_agent.rb b/gems/smithy-client/lib/smithy-client/plugins/user_agent.rb new file mode 100644 index 000000000..683c671e4 --- /dev/null +++ b/gems/smithy-client/lib/smithy-client/plugins/user_agent.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module Smithy + module Client + module Plugins + # @api private + class UserAgent < Plugin + option( + :user_agent_suffix, + doc_type: String, + docstring: <<~DOCS) + An optional string that is appended to the User-Agent header. + The default User-Agent includes the smithy-client version, + the ruby platform and version, and host OS information. + DOCS + + # @api private + class Handler < Client::Handler + def call(context) + context.http_request.headers['User-Agent'] = UserAgent.new(context).to_s + @handler.call(context) + end + + # @api private + class UserAgent + def initialize(context) + @context = context + end + + def to_s + ua = "smithy-ruby/#{Smithy::Client::VERSION}" + ua += " (#{os_metadata};" + ua += " #{language_metadata})" + ua += " #{@context.config.user_agent_suffix}" if @context.config.user_agent_suffix + ua.strip + end + + private + + def os_metadata + os = + case RbConfig::CONFIG['host_os'] + when /mac|darwin/ + 'macos' + when /linux|cygwin/ + 'linux' + when /mingw|mswin/ + 'windows' + else + 'other' + end + metadata = os.to_s + local_version = Gem::Platform.local.version + metadata += " #{local_version}" if local_version + metadata + "; #{RbConfig::CONFIG['host_cpu']}" + end + + def language_metadata + "ruby/#{RUBY_VERSION}" + end + end + end + + handler(Handler, step: :sign, priority: 5) + end + end + end +end diff --git a/gems/smithy-client/spec/smithy-client/plugins/user_agent_spec.rb b/gems/smithy-client/spec/smithy-client/plugins/user_agent_spec.rb new file mode 100644 index 000000000..c7d8db060 --- /dev/null +++ b/gems/smithy-client/spec/smithy-client/plugins/user_agent_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require_relative '../../spec_helper' + +module Smithy + module Client + module Plugins + describe UserAgent do + let(:client_class) { ClientHelper.sample_client.const_get(:Client) } + let(:client) { client_class.new(stub_responses: true) } + + it 'adds a user agent header to request' do + resp = client.operation + ua_header = resp.context.http_request.headers['user-agent'] + expect(ua_header).to_not be_nil + expect(ua_header).to include('smithy-ruby') + expect(ua_header).to include('macos').or include('linux').or include('windows').or include('other') + expect(ua_header).to include('ruby') + end + + it 'adds a user agent suffix to user agent string when configured' do + client = client_class.new(user_agent_suffix: 'test-suffix', stub_responses: true) + resp = client.operation + ua_header = resp.context.http_request.headers['user-agent'] + expect(ua_header).to end_with('test-suffix') + end + end + end + end +end diff --git a/gems/smithy/lib/smithy/welds/plugins.rb b/gems/smithy/lib/smithy/welds/plugins.rb index 6ecc8e1fa..f6e8c451c 100644 --- a/gems/smithy/lib/smithy/welds/plugins.rb +++ b/gems/smithy/lib/smithy/welds/plugins.rb @@ -17,6 +17,7 @@ require 'smithy-client/plugins/response_target' require 'smithy-client/plugins/retry_errors' require 'smithy-client/plugins/stub_responses' +require 'smithy-client/plugins/user_agent' module Smithy module Welds @@ -27,7 +28,7 @@ def for?(_service) true end - def add_plugins + def add_plugins # rubocop:disable Metrics/MethodLength base_path = 'smithy-client/plugins' { Smithy::Client::Plugins::ChecksumRequired => { require_path: "#{base_path}/checksum_required" }, @@ -46,7 +47,8 @@ def add_plugins Smithy::Client::Plugins::ResolveAuth => { require_path: "#{base_path}/resolve_auth" }, Smithy::Client::Plugins::ResponseTarget => { require_path: "#{base_path}/response_target" }, Smithy::Client::Plugins::RetryErrors => { require_path: "#{base_path}/retry_errors" }, - Smithy::Client::Plugins::StubResponses => { require_path: "#{base_path}/stub_responses" } + Smithy::Client::Plugins::StubResponses => { require_path: "#{base_path}/stub_responses" }, + Smithy::Client::Plugins::UserAgent => { require_path: "#{base_path}/user_agent" } } end end diff --git a/projections/shapes/lib/shapes/client.rb b/projections/shapes/lib/shapes/client.rb index 6f518b2a5..13d353430 100644 --- a/projections/shapes/lib/shapes/client.rb +++ b/projections/shapes/lib/shapes/client.rb @@ -20,6 +20,7 @@ require 'smithy-client/plugins/response_target' require 'smithy-client/plugins/retry_errors' require 'smithy-client/plugins/stub_responses' +require 'smithy-client/plugins/user_agent' module ShapeService # An API client for ShapeService. @@ -47,6 +48,7 @@ class Client < Smithy::Client::Base add_plugin(Smithy::Client::Plugins::ResponseTarget) add_plugin(Smithy::Client::Plugins::RetryErrors) add_plugin(Smithy::Client::Plugins::StubResponses) + add_plugin(Smithy::Client::Plugins::UserAgent) # @param options [Hash] Client options # @option options [Boolean] :adaptive_retry_wait_to_fill (true) @@ -158,6 +160,10 @@ class Client < Smithy::Client::Base # By default fake responses are generated and returned. You can specify the response data # to return or errors to raise by calling {Stubs#stub_responses}. # @see Stubs + # @option options [String] :user_agent_suffix + # An optional string that is appended to the User-Agent header. + # The default User-Agent includes the smithy-client version, + # the ruby platform and version, and host OS information. # @option options [Boolean] :validate_params (true) # When `true`, request parameters are validated before sending the request. def initialize(*options) diff --git a/projections/shapes/sig/shapes/client.rbs b/projections/shapes/sig/shapes/client.rbs index 9fa100136..ec08f21b4 100644 --- a/projections/shapes/sig/shapes/client.rbs +++ b/projections/shapes/sig/shapes/client.rbs @@ -34,6 +34,7 @@ module ShapeService ?retry_max_attempts: Integer, ?retry_strategy: String | Class, ?stub_responses: bool, + ?user_agent_suffix: String, ?validate_params: bool, ) -> void | (?Hash[Symbol, untyped]) -> void diff --git a/projections/weather/lib/weather/client.rb b/projections/weather/lib/weather/client.rb index 231a2f2bc..b2d2c06a4 100644 --- a/projections/weather/lib/weather/client.rb +++ b/projections/weather/lib/weather/client.rb @@ -20,6 +20,7 @@ require 'smithy-client/plugins/response_target' require 'smithy-client/plugins/retry_errors' require 'smithy-client/plugins/stub_responses' +require 'smithy-client/plugins/user_agent' module Weather # An API client for Weather. @@ -47,6 +48,7 @@ class Client < Smithy::Client::Base add_plugin(Smithy::Client::Plugins::ResponseTarget) add_plugin(Smithy::Client::Plugins::RetryErrors) add_plugin(Smithy::Client::Plugins::StubResponses) + add_plugin(Smithy::Client::Plugins::UserAgent) # @param options [Hash] Client options # @option options [Boolean] :adaptive_retry_wait_to_fill (true) @@ -158,6 +160,10 @@ class Client < Smithy::Client::Base # By default fake responses are generated and returned. You can specify the response data # to return or errors to raise by calling {Stubs#stub_responses}. # @see Stubs + # @option options [String] :user_agent_suffix + # An optional string that is appended to the User-Agent header. + # The default User-Agent includes the smithy-client version, + # the ruby platform and version, and host OS information. # @option options [Boolean] :validate_params (true) # When `true`, request parameters are validated before sending the request. def initialize(*options) diff --git a/projections/weather/sig/weather/client.rbs b/projections/weather/sig/weather/client.rbs index 9dbfa5cec..79293ecdc 100644 --- a/projections/weather/sig/weather/client.rbs +++ b/projections/weather/sig/weather/client.rbs @@ -34,6 +34,7 @@ module Weather ?retry_max_attempts: Integer, ?retry_strategy: String | Class, ?stub_responses: bool, + ?user_agent_suffix: String, ?validate_params: bool, ) -> void | (?Hash[Symbol, untyped]) -> void