From 6a354021611fea08d23ad2fdc35750c7b530504c Mon Sep 17 00:00:00 2001 From: Matt Muller <53055821+mullermp@users.noreply.github.com> Date: Thu, 11 Jul 2024 13:11:09 -0400 Subject: [PATCH] Global config for Hearth clients (#207) --- .../rails_json/lib/rails_json/client.rb | 6 +- .../lib/rails_json/plugins/global_config.rb | 26 +++ .../rpcv2_cbor/lib/rpcv2_cbor/client.rb | 6 +- .../lib/rpcv2_cbor/plugins/global_config.rb | 26 +++ .../white_label/lib/white_label/client.rb | 2 + .../lib/white_label/plugins/global_config.rb | 26 +++ .../white_label/spec/client_spec.rb | 204 +++++------------- .../white_label/spec/middleware_spec.rb | 117 ++++++++++ .../WhiteLabelTestIntegration.java | 2 +- .../smithy-ruby-codegen-test/build.gradle.kts | 1 + .../integration-specs/client_spec.rb | 204 +++++------------- .../integration-specs/middleware_spec.rb | 117 ++++++++++ .../ruby/codegen/DirectedRubyCodegen.java | 4 + .../ruby/codegen/GenerationContext.java | 15 +- .../amazon/smithy/ruby/codegen/Hearth.java | 4 + .../smithy/ruby/codegen/RubyCodeWriter.java | 2 +- .../smithy/ruby/codegen/RubyIntegration.java | 2 +- .../GlobalConfigPluginGenerator.java | 82 +++++++ hearth/.rspec | 2 +- hearth/.rubocop.yml | 4 + hearth/lib/hearth.rb | 20 ++ hearth/lib/hearth/client.rb | 2 + hearth/lib/hearth/config/resolver.rb | 5 +- hearth/lib/hearth/configuration.rb | 4 + hearth/sig/lib/hearth.rbs | 7 + hearth/sig/lib/hearth/configuration.rbs | 2 + hearth/spec/hearth/cbor/cbor_spec.rb | 2 - hearth/spec/hearth/cbor/decoder_spec.rb | 2 - hearth/spec/hearth/cbor/encoder_spec.rb | 2 - hearth/spec/{ => hearth}/client_spec.rb | 6 +- hearth/spec/hearth_spec.rb | 18 ++ hearth/spec/spec_helper.rb | 105 ++++----- tasks/test.rake | 21 +- 33 files changed, 655 insertions(+), 393 deletions(-) create mode 100644 codegen/projections/rails_json/lib/rails_json/plugins/global_config.rb create mode 100644 codegen/projections/rpcv2_cbor/lib/rpcv2_cbor/plugins/global_config.rb create mode 100644 codegen/projections/white_label/lib/white_label/plugins/global_config.rb create mode 100644 codegen/projections/white_label/spec/middleware_spec.rb create mode 100644 codegen/smithy-ruby-codegen-test/integration-specs/middleware_spec.rb create mode 100644 codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/GlobalConfigPluginGenerator.java create mode 100644 hearth/sig/lib/hearth.rbs rename hearth/spec/{ => hearth}/client_spec.rb (97%) create mode 100644 hearth/spec/hearth_spec.rb diff --git a/codegen/projections/rails_json/lib/rails_json/client.rb b/codegen/projections/rails_json/lib/rails_json/client.rb index 2851dc469..fc9e58c83 100644 --- a/codegen/projections/rails_json/lib/rails_json/client.rb +++ b/codegen/projections/rails_json/lib/rails_json/client.rb @@ -9,12 +9,16 @@ require 'stringio' +require_relative 'plugins/global_config' + module RailsJson # A REST JSON service that sends JSON requests and responses. class Client < Hearth::Client # @api private - @plugins = Hearth::PluginList.new + @plugins = Hearth::PluginList.new([ + Plugins::GlobalConfig.new + ]) # @param [Hash] options # Options used to construct an instance of {Config} diff --git a/codegen/projections/rails_json/lib/rails_json/plugins/global_config.rb b/codegen/projections/rails_json/lib/rails_json/plugins/global_config.rb new file mode 100644 index 000000000..8114b8745 --- /dev/null +++ b/codegen/projections/rails_json/lib/rails_json/plugins/global_config.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# WARNING ABOUT GENERATED CODE +# +# This file was code generated using smithy-ruby. +# https://github.com/smithy-lang/smithy-ruby +# +# WARNING ABOUT GENERATED CODE + +module RailsJson + module Plugins + # GlobalConfig plugin sets default values for the {Client}'s config. + # See {Hearth#config=} for setting default values. + # + class GlobalConfig + + def call(config) + options = config.options + ::Hearth.config.each do |key, value| + config[key] = value unless options.key?(key) + end + end + + end + end +end diff --git a/codegen/projections/rpcv2_cbor/lib/rpcv2_cbor/client.rb b/codegen/projections/rpcv2_cbor/lib/rpcv2_cbor/client.rb index 2e1b4d5ad..d23698254 100644 --- a/codegen/projections/rpcv2_cbor/lib/rpcv2_cbor/client.rb +++ b/codegen/projections/rpcv2_cbor/lib/rpcv2_cbor/client.rb @@ -9,11 +9,15 @@ require 'stringio' +require_relative 'plugins/global_config' + module Rpcv2Cbor class Client < Hearth::Client # @api private - @plugins = Hearth::PluginList.new + @plugins = Hearth::PluginList.new([ + Plugins::GlobalConfig.new + ]) # @param [Hash] options # Options used to construct an instance of {Config} diff --git a/codegen/projections/rpcv2_cbor/lib/rpcv2_cbor/plugins/global_config.rb b/codegen/projections/rpcv2_cbor/lib/rpcv2_cbor/plugins/global_config.rb new file mode 100644 index 000000000..ade9ac355 --- /dev/null +++ b/codegen/projections/rpcv2_cbor/lib/rpcv2_cbor/plugins/global_config.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# WARNING ABOUT GENERATED CODE +# +# This file was code generated using smithy-ruby. +# https://github.com/smithy-lang/smithy-ruby +# +# WARNING ABOUT GENERATED CODE + +module Rpcv2Cbor + module Plugins + # GlobalConfig plugin sets default values for the {Client}'s config. + # See {Hearth#config=} for setting default values. + # + class GlobalConfig + + def call(config) + options = config.options + ::Hearth.config.each do |key, value| + config[key] = value unless options.key?(key) + end + end + + end + end +end diff --git a/codegen/projections/white_label/lib/white_label/client.rb b/codegen/projections/white_label/lib/white_label/client.rb index fbdef2be6..0729b13d3 100644 --- a/codegen/projections/white_label/lib/white_label/client.rb +++ b/codegen/projections/white_label/lib/white_label/client.rb @@ -9,6 +9,7 @@ require 'stringio' +require_relative 'plugins/global_config' require_relative 'plugins/test_plugin' module WhiteLabel @@ -29,6 +30,7 @@ class Client < Hearth::Client # @api private @plugins = Hearth::PluginList.new([ + Plugins::GlobalConfig.new, Plugins::TestPlugin.new ]) diff --git a/codegen/projections/white_label/lib/white_label/plugins/global_config.rb b/codegen/projections/white_label/lib/white_label/plugins/global_config.rb new file mode 100644 index 000000000..a7ca383c8 --- /dev/null +++ b/codegen/projections/white_label/lib/white_label/plugins/global_config.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# WARNING ABOUT GENERATED CODE +# +# This file was code generated using smithy-ruby. +# https://github.com/smithy-lang/smithy-ruby +# +# WARNING ABOUT GENERATED CODE + +module WhiteLabel + module Plugins + # GlobalConfig plugin sets default values for the {Client}'s config. + # See {Hearth#config=} for setting default values. + # + class GlobalConfig + + def call(config) + options = config.options + ::Hearth.config.each do |key, value| + config[key] = value unless options.key?(key) + end + end + + end + end +end diff --git a/codegen/projections/white_label/spec/client_spec.rb b/codegen/projections/white_label/spec/client_spec.rb index 15b5a4407..2b27755c6 100644 --- a/codegen/projections/white_label/spec/client_spec.rb +++ b/codegen/projections/white_label/spec/client_spec.rb @@ -6,173 +6,69 @@ module WhiteLabel describe Client do let(:client) { Client.new(stub_responses: true) } - describe '#kitchen_sink' do - it 'validates config' do - expect do - Client.new(stub_responses: 'false') - end.to raise_error(ArgumentError, /config\[:stub_responses\]/) - end + it 'uses logger' do + expect(client.config.logger) + .to receive(:debug) + .with(anything) + .at_least(:once) + expect(client.config.logger) + .to receive(:info) + .with(anything) + .at_least(:once) + + client.kitchen_sink + end - it 'uses validate_input' do - expect(Hearth::Middleware::Validate) - .to receive(:new) - .with(anything, - validate_input: client.config.validate_input, - validator: anything) - .and_call_original + it 'validates config' do + expect do + Client.new(stub_responses: 'false') + end.to raise_error(ArgumentError, /config\[:stub_responses\]/) + end - client.kitchen_sink - end + context 'global config' do + after { Hearth.config = {} } - it 'uses retry_strategy' do - expect(Hearth::Middleware::Retry) - .to receive(:new) - .with(anything, - retry_strategy: client.config.retry_strategy, - error_inspector_class: anything) - .and_call_original - - client.kitchen_sink + it 'allows for global configuration' do + logger = Logger.new(IO::NULL, level: :debug) + Hearth.config[:logger] = logger + expect(client.config.logger).to eq(logger) end - # it 'uses resolver, schemes, and identity resolvers' do - # expect(Hearth::Middleware::Auth) - # .to receive(:new) - # .with(anything, - # auth_params: anything, - # auth_resolver: client.config.auth_resolver, - # auth_schemes: client.config.auth_schemes, - # Hearth::Identities::HTTPLogin => - # client.config.http_login_provider, - # Hearth::Identities::HTTPBearer => - # client.config.http_bearer_provider, - # Hearth::Identities::HTTPApiKey => - # client.config.http_api_key_provider, - # Auth::HTTPCustomKey => - # client.config.http_custom_key_provider) - # .and_call_original - # - # client.kitchen_sink - # end - - it 'uses stub_responses and transmission client' do - expect(Hearth::Middleware::Send) - .to receive(:new) - .with(anything, - stub_responses: client.config.stub_responses, - client: client.config.http_client, - stub_error_classes: anything, - stub_data_class: anything, - stubs: anything) - .and_call_original - - client.kitchen_sink + it 'validates global config values' do + Hearth.config[:logger] = 'logger' + expect do + Client.new + end.to raise_error(ArgumentError, /config\[:logger\]/) end - it 'uses logger' do - expect(client.config.logger) - .to receive(:debug) - .with(anything) - .at_least(:once) - expect(client.config.logger) - .to receive(:info) - .with(anything) - .at_least(:once) - - client.kitchen_sink + it 'is overridden by client config' do + Hearth.config[:logger] = Logger.new(IO::NULL, level: :debug) + logger = Logger.new(IO::NULL, level: :info) + client = Client.new(logger: logger) + expect(client.config.logger).to eq(logger) end + end - it 'uses endpoint' do - proc = proc do |context| - expect(context.request.uri) - .to eq(URI(client.config.endpoint)) - end - interceptor = Hearth::Interceptor.new(read_before_transmit: proc) - client.kitchen_sink({}, interceptors: [interceptor]) + context 'operation overrides' do + it 'validates config' do + expect do + client.kitchen_sink({}, endpoint: 1) + end.to raise_error(ArgumentError, /config\[:endpoint\]/) end - context 'operation overrides' do - it 'validates config' do - expect do - client.kitchen_sink({}, endpoint: 1) - end.to raise_error(ArgumentError, /config\[:endpoint\]/) - end - - it 'uses validate_input from options' do - expect(Hearth::Middleware::Validate) - .to receive(:new) - .with(anything, - validate_input: false, - validator: anything) - .and_call_original - - client.kitchen_sink({}, validate_input: false) - end - - it 'uses retry_strategy from options' do - retry_strategy = Hearth::Retry::Adaptive.new - expect(Hearth::Middleware::Retry) - .to receive(:new) - .with(anything, - retry_strategy: retry_strategy, - error_inspector_class: anything) - .and_call_original - - client.kitchen_sink({}, retry_strategy: retry_strategy) - end - - it 'uses transmission client from options' do - http_client = Hearth::HTTP::Client.new - expect(Hearth::Middleware::Send) - .to receive(:new) - .with(anything, - stub_responses: true, - client: http_client, - stub_error_classes: anything, - stub_data_class: anything, - stubs: anything) - .and_call_original - - client.kitchen_sink( - {}, http_client: http_client - ) - end - - it 'uses logger from options' do - logger = Logger.new(IO::NULL, level: :debug) - expect(logger).to receive(:debug).with(anything).at_least(:once) - expect(logger).to receive(:info).with(anything).at_least(:once) - - client.kitchen_sink({}, logger: logger) - end - - it 'uses endpoint from options' do - proc = proc do |context| - expect(context.request.uri) - .to eq(URI('https://override.com')) - end - - interceptor = Hearth::Interceptor.new(read_before_transmit: proc) - client.kitchen_sink( - {}, - endpoint: 'https://override.com', - interceptors: [interceptor] - ) - end + it 'uses config from options' do + operation_config = { endpoint: 'https://example.com' } + allow_any_instance_of(Config).to receive(:freeze) + expect_any_instance_of(Config) + .to receive(:merge).with(operation_config) + .and_call_original + client.kitchen_sink({}, operation_config) end - end - context '#relative_middleware' do - it 'allows a specific order of middleware to their relatives' do - output = client.relative_middleware - expect(output.metadata[:middleware_order]) - .to eq( - [ - WhiteLabel::Middleware::BeforeMiddleware, - WhiteLabel::Middleware::MidMiddleware, - WhiteLabel::Middleware::AfterMiddleware - ] - ) + it 'raises when given stubs or stub responses' do + expect do + client.kitchen_sink({}, stub_responses: true) + end.to raise_error(ArgumentError, /stubs or stub_responses/) end end end diff --git a/codegen/projections/white_label/spec/middleware_spec.rb b/codegen/projections/white_label/spec/middleware_spec.rb new file mode 100644 index 000000000..b6ee76aac --- /dev/null +++ b/codegen/projections/white_label/spec/middleware_spec.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require_relative 'spec_helper' + +module WhiteLabel + describe Client do + let(:client) { Client.new(stub_responses: true) } + + it 'uses auth resolver, schemes, and identity resolvers' do + expect(Hearth::Middleware::Auth) + .to receive(:new) + .with(anything, + auth_params: anything, + auth_resolver: client.config.auth_resolver, + auth_schemes: client.config.auth_schemes, + Hearth::Identities::HTTPLogin => + client.config.http_login_provider, + Hearth::Identities::HTTPBearer => + client.config.http_bearer_provider, + Hearth::Identities::HTTPApiKey => + client.config.http_api_key_provider, + Auth::HTTPCustomKey => + client.config.http_custom_key_provider) + .and_call_original + + client.kitchen_sink + end + + it 'uses disable_host_prefix' do + expect(Hearth::Middleware::HostPrefix) + .to receive(:new) + .with(anything, + disable_host_prefix: client.config.disable_host_prefix, + host_prefix: anything) + .and_call_original + + client.endpoint_operation + end + + it 'uses disable_request_compression and ' \ + 'request_min_compression_size_bytes' do + expect(Hearth::HTTP::Middleware::RequestCompression) + .to receive(:new) + .with(anything, + disable_request_compression: + client.config.disable_request_compression, + request_min_compression_size_bytes: + client.config.request_min_compression_size_bytes, + encodings: anything, + streaming: anything) + .and_call_original + + client.request_compression + end + + it 'uses endpoint and endpoint_resolver' do + expect(Hearth::Middleware::Endpoint) + .to receive(:new) + .with(anything, + endpoint: client.config.endpoint, + endpoint_resolver: client.config.endpoint_resolver, + param_builder: anything, + stage: anything) + .and_call_original + + client.kitchen_sink + end + + it 'uses stubs, stub_responses, and http client' do + expect(Hearth::Middleware::Send) + .to receive(:new) + .with(anything, + stubs: client.config.stubs, + stub_responses: client.config.stub_responses, + client: client.config.http_client, + stub_error_classes: anything, + stub_data_class: anything) + .and_call_original + + client.kitchen_sink + end + + it 'uses retry_strategy' do + expect(Hearth::Middleware::Retry) + .to receive(:new) + .with(anything, + retry_strategy: client.config.retry_strategy, + error_inspector_class: anything) + .and_call_original + + client.kitchen_sink + end + + it 'uses validate_input' do + expect(Hearth::Middleware::Validate) + .to receive(:new) + .with(anything, + validate_input: client.config.validate_input, + validator: anything) + .and_call_original + + client.kitchen_sink + end + + it 'allows a specific order of middleware to their relatives' do + output = client.relative_middleware + expect(output.metadata[:middleware_order]) + .to eq( + [ + WhiteLabel::Middleware::BeforeMiddleware, + WhiteLabel::Middleware::MidMiddleware, + WhiteLabel::Middleware::AfterMiddleware + ] + ) + end + end +end diff --git a/codegen/smithy-ruby-codegen-test-utils/src/main/java/software/amazon/smithy/ruby/codegen/integrations/WhiteLabelTestIntegration.java b/codegen/smithy-ruby-codegen-test-utils/src/main/java/software/amazon/smithy/ruby/codegen/integrations/WhiteLabelTestIntegration.java index adfc84250..f955344ac 100644 --- a/codegen/smithy-ruby-codegen-test-utils/src/main/java/software/amazon/smithy/ruby/codegen/integrations/WhiteLabelTestIntegration.java +++ b/codegen/smithy-ruby-codegen-test-utils/src/main/java/software/amazon/smithy/ruby/codegen/integrations/WhiteLabelTestIntegration.java @@ -42,7 +42,7 @@ public boolean includeFor(ServiceShape service, Model model) { } @Override - public List getRuntimePlugins(GenerationContext context) { + public List getAdditionalRuntimePlugins(GenerationContext context) { return List.of(RubyRuntimePlugin.builder() .rubySource("plugins/test_plugin.rb") .pluginClass("Plugins::TestPlugin") diff --git a/codegen/smithy-ruby-codegen-test/build.gradle.kts b/codegen/smithy-ruby-codegen-test/build.gradle.kts index ff7f8f807..bf0811f94 100644 --- a/codegen/smithy-ruby-codegen-test/build.gradle.kts +++ b/codegen/smithy-ruby-codegen-test/build.gradle.kts @@ -160,6 +160,7 @@ tasks.register("copyRpcv2CborGem") { tasks.register("cleanProjections") { delete("$buildDir/../../projections/white_label/") + delete("$buildDir/../../projections/rpcv2_cbor/") } tasks.register("copyIntegrationSpecs") { 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 15b5a4407..2b27755c6 100644 --- a/codegen/smithy-ruby-codegen-test/integration-specs/client_spec.rb +++ b/codegen/smithy-ruby-codegen-test/integration-specs/client_spec.rb @@ -6,173 +6,69 @@ module WhiteLabel describe Client do let(:client) { Client.new(stub_responses: true) } - describe '#kitchen_sink' do - it 'validates config' do - expect do - Client.new(stub_responses: 'false') - end.to raise_error(ArgumentError, /config\[:stub_responses\]/) - end + it 'uses logger' do + expect(client.config.logger) + .to receive(:debug) + .with(anything) + .at_least(:once) + expect(client.config.logger) + .to receive(:info) + .with(anything) + .at_least(:once) + + client.kitchen_sink + end - it 'uses validate_input' do - expect(Hearth::Middleware::Validate) - .to receive(:new) - .with(anything, - validate_input: client.config.validate_input, - validator: anything) - .and_call_original + it 'validates config' do + expect do + Client.new(stub_responses: 'false') + end.to raise_error(ArgumentError, /config\[:stub_responses\]/) + end - client.kitchen_sink - end + context 'global config' do + after { Hearth.config = {} } - it 'uses retry_strategy' do - expect(Hearth::Middleware::Retry) - .to receive(:new) - .with(anything, - retry_strategy: client.config.retry_strategy, - error_inspector_class: anything) - .and_call_original - - client.kitchen_sink + it 'allows for global configuration' do + logger = Logger.new(IO::NULL, level: :debug) + Hearth.config[:logger] = logger + expect(client.config.logger).to eq(logger) end - # it 'uses resolver, schemes, and identity resolvers' do - # expect(Hearth::Middleware::Auth) - # .to receive(:new) - # .with(anything, - # auth_params: anything, - # auth_resolver: client.config.auth_resolver, - # auth_schemes: client.config.auth_schemes, - # Hearth::Identities::HTTPLogin => - # client.config.http_login_provider, - # Hearth::Identities::HTTPBearer => - # client.config.http_bearer_provider, - # Hearth::Identities::HTTPApiKey => - # client.config.http_api_key_provider, - # Auth::HTTPCustomKey => - # client.config.http_custom_key_provider) - # .and_call_original - # - # client.kitchen_sink - # end - - it 'uses stub_responses and transmission client' do - expect(Hearth::Middleware::Send) - .to receive(:new) - .with(anything, - stub_responses: client.config.stub_responses, - client: client.config.http_client, - stub_error_classes: anything, - stub_data_class: anything, - stubs: anything) - .and_call_original - - client.kitchen_sink + it 'validates global config values' do + Hearth.config[:logger] = 'logger' + expect do + Client.new + end.to raise_error(ArgumentError, /config\[:logger\]/) end - it 'uses logger' do - expect(client.config.logger) - .to receive(:debug) - .with(anything) - .at_least(:once) - expect(client.config.logger) - .to receive(:info) - .with(anything) - .at_least(:once) - - client.kitchen_sink + it 'is overridden by client config' do + Hearth.config[:logger] = Logger.new(IO::NULL, level: :debug) + logger = Logger.new(IO::NULL, level: :info) + client = Client.new(logger: logger) + expect(client.config.logger).to eq(logger) end + end - it 'uses endpoint' do - proc = proc do |context| - expect(context.request.uri) - .to eq(URI(client.config.endpoint)) - end - interceptor = Hearth::Interceptor.new(read_before_transmit: proc) - client.kitchen_sink({}, interceptors: [interceptor]) + context 'operation overrides' do + it 'validates config' do + expect do + client.kitchen_sink({}, endpoint: 1) + end.to raise_error(ArgumentError, /config\[:endpoint\]/) end - context 'operation overrides' do - it 'validates config' do - expect do - client.kitchen_sink({}, endpoint: 1) - end.to raise_error(ArgumentError, /config\[:endpoint\]/) - end - - it 'uses validate_input from options' do - expect(Hearth::Middleware::Validate) - .to receive(:new) - .with(anything, - validate_input: false, - validator: anything) - .and_call_original - - client.kitchen_sink({}, validate_input: false) - end - - it 'uses retry_strategy from options' do - retry_strategy = Hearth::Retry::Adaptive.new - expect(Hearth::Middleware::Retry) - .to receive(:new) - .with(anything, - retry_strategy: retry_strategy, - error_inspector_class: anything) - .and_call_original - - client.kitchen_sink({}, retry_strategy: retry_strategy) - end - - it 'uses transmission client from options' do - http_client = Hearth::HTTP::Client.new - expect(Hearth::Middleware::Send) - .to receive(:new) - .with(anything, - stub_responses: true, - client: http_client, - stub_error_classes: anything, - stub_data_class: anything, - stubs: anything) - .and_call_original - - client.kitchen_sink( - {}, http_client: http_client - ) - end - - it 'uses logger from options' do - logger = Logger.new(IO::NULL, level: :debug) - expect(logger).to receive(:debug).with(anything).at_least(:once) - expect(logger).to receive(:info).with(anything).at_least(:once) - - client.kitchen_sink({}, logger: logger) - end - - it 'uses endpoint from options' do - proc = proc do |context| - expect(context.request.uri) - .to eq(URI('https://override.com')) - end - - interceptor = Hearth::Interceptor.new(read_before_transmit: proc) - client.kitchen_sink( - {}, - endpoint: 'https://override.com', - interceptors: [interceptor] - ) - end + it 'uses config from options' do + operation_config = { endpoint: 'https://example.com' } + allow_any_instance_of(Config).to receive(:freeze) + expect_any_instance_of(Config) + .to receive(:merge).with(operation_config) + .and_call_original + client.kitchen_sink({}, operation_config) end - end - context '#relative_middleware' do - it 'allows a specific order of middleware to their relatives' do - output = client.relative_middleware - expect(output.metadata[:middleware_order]) - .to eq( - [ - WhiteLabel::Middleware::BeforeMiddleware, - WhiteLabel::Middleware::MidMiddleware, - WhiteLabel::Middleware::AfterMiddleware - ] - ) + it 'raises when given stubs or stub responses' do + expect do + client.kitchen_sink({}, stub_responses: true) + end.to raise_error(ArgumentError, /stubs or stub_responses/) end end end diff --git a/codegen/smithy-ruby-codegen-test/integration-specs/middleware_spec.rb b/codegen/smithy-ruby-codegen-test/integration-specs/middleware_spec.rb new file mode 100644 index 000000000..b6ee76aac --- /dev/null +++ b/codegen/smithy-ruby-codegen-test/integration-specs/middleware_spec.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require_relative 'spec_helper' + +module WhiteLabel + describe Client do + let(:client) { Client.new(stub_responses: true) } + + it 'uses auth resolver, schemes, and identity resolvers' do + expect(Hearth::Middleware::Auth) + .to receive(:new) + .with(anything, + auth_params: anything, + auth_resolver: client.config.auth_resolver, + auth_schemes: client.config.auth_schemes, + Hearth::Identities::HTTPLogin => + client.config.http_login_provider, + Hearth::Identities::HTTPBearer => + client.config.http_bearer_provider, + Hearth::Identities::HTTPApiKey => + client.config.http_api_key_provider, + Auth::HTTPCustomKey => + client.config.http_custom_key_provider) + .and_call_original + + client.kitchen_sink + end + + it 'uses disable_host_prefix' do + expect(Hearth::Middleware::HostPrefix) + .to receive(:new) + .with(anything, + disable_host_prefix: client.config.disable_host_prefix, + host_prefix: anything) + .and_call_original + + client.endpoint_operation + end + + it 'uses disable_request_compression and ' \ + 'request_min_compression_size_bytes' do + expect(Hearth::HTTP::Middleware::RequestCompression) + .to receive(:new) + .with(anything, + disable_request_compression: + client.config.disable_request_compression, + request_min_compression_size_bytes: + client.config.request_min_compression_size_bytes, + encodings: anything, + streaming: anything) + .and_call_original + + client.request_compression + end + + it 'uses endpoint and endpoint_resolver' do + expect(Hearth::Middleware::Endpoint) + .to receive(:new) + .with(anything, + endpoint: client.config.endpoint, + endpoint_resolver: client.config.endpoint_resolver, + param_builder: anything, + stage: anything) + .and_call_original + + client.kitchen_sink + end + + it 'uses stubs, stub_responses, and http client' do + expect(Hearth::Middleware::Send) + .to receive(:new) + .with(anything, + stubs: client.config.stubs, + stub_responses: client.config.stub_responses, + client: client.config.http_client, + stub_error_classes: anything, + stub_data_class: anything) + .and_call_original + + client.kitchen_sink + end + + it 'uses retry_strategy' do + expect(Hearth::Middleware::Retry) + .to receive(:new) + .with(anything, + retry_strategy: client.config.retry_strategy, + error_inspector_class: anything) + .and_call_original + + client.kitchen_sink + end + + it 'uses validate_input' do + expect(Hearth::Middleware::Validate) + .to receive(:new) + .with(anything, + validate_input: client.config.validate_input, + validator: anything) + .and_call_original + + client.kitchen_sink + end + + it 'allows a specific order of middleware to their relatives' do + output = client.relative_middleware + expect(output.metadata[:middleware_order]) + .to eq( + [ + WhiteLabel::Middleware::BeforeMiddleware, + WhiteLabel::Middleware::MidMiddleware, + WhiteLabel::Middleware::AfterMiddleware + ] + ) + end + end +end 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 200b506d7..b9daa8320 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 @@ -42,6 +42,7 @@ import software.amazon.smithy.ruby.codegen.generators.ConfigGenerator; import software.amazon.smithy.ruby.codegen.generators.EndpointGenerator; import software.amazon.smithy.ruby.codegen.generators.GemspecGenerator; +import software.amazon.smithy.ruby.codegen.generators.GlobalConfigPluginGenerator; import software.amazon.smithy.ruby.codegen.generators.HttpProtocolTestGenerator; import software.amazon.smithy.ruby.codegen.generators.MiddlewareGenerator; import software.amazon.smithy.ruby.codegen.generators.ModuleGenerator; @@ -166,6 +167,9 @@ public void generateService(GenerateServiceDirective getRubyDependencies() { * @return Set of all RubyRuntimePlugins from all integrations */ public Set getRuntimePlugins() { - return integrations.stream() - .map((i) -> i.getRuntimePlugins(this)) - .flatMap(List::stream) - .collect(Collectors.toUnmodifiableSet()); + Set runtimePlugins = new LinkedHashSet<>(); + runtimePlugins.add(RubyRuntimePlugin.builder() + .pluginClass("Plugins::GlobalConfig") + .writeAdditionalFiles(context -> Collections.singletonList("plugins/global_config.rb")) + .build()); + integrations.forEach((i) -> { + runtimePlugins.addAll(i.getAdditionalRuntimePlugins(this)); + }); + return Collections.unmodifiableSet(runtimePlugins); } - /** * @return Set of AuthSchemes that apply to this service. */ 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 b12cc0293..b168cab3f 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 @@ -22,6 +22,10 @@ */ public final class Hearth { + public static final Symbol HEARTH = Symbol.builder() + .name("Hearth") + .build(); + public static final Symbol CLIENT = Symbol.builder() .namespace("Hearth", "::") .name("Client") 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 c6c99a6cb..bdf36d71f 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 @@ -129,7 +129,7 @@ public RubyCodeWriter preamble() { } /** - * Require statments for symbols/dependenices used + * Require statements for symbols/dependencies used * 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/RubyIntegration.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/RubyIntegration.java index 0caaa164c..d2267c8f3 100644 --- a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/RubyIntegration.java +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/RubyIntegration.java @@ -81,7 +81,7 @@ default List getAdditionalClientConfig(GenerationContext context) * @param context - Generation context to process within * @return List of RubyRuntimePlugins */ - default List getRuntimePlugins(GenerationContext context) { + default List getAdditionalRuntimePlugins(GenerationContext context) { return Collections.emptyList(); } diff --git a/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/GlobalConfigPluginGenerator.java b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/GlobalConfigPluginGenerator.java new file mode 100644 index 000000000..b52c5e398 --- /dev/null +++ b/codegen/smithy-ruby-codegen/src/main/java/software/amazon/smithy/ruby/codegen/generators/GlobalConfigPluginGenerator.java @@ -0,0 +1,82 @@ +/* + * 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.nio.file.Paths; +import java.util.logging.Logger; +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; +import software.amazon.smithy.ruby.codegen.RubyFormatter; +import software.amazon.smithy.ruby.codegen.RubySettings; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Generate Config class for a Client. + */ +@SmithyInternalApi +public class GlobalConfigPluginGenerator extends RubyGeneratorBase { + private static final Logger LOGGER = + Logger.getLogger(ConfigGenerator.class.getName()); + + public GlobalConfigPluginGenerator( + ContextualDirective directive) { + super(directive); + } + + @Override + protected String getModule() { + return "Plugins"; + } + + @Override + public String rbFile() { + return Paths.get(settings.getGemName(), "lib", settings.getGemName(), + RubyFormatter.toSnakeCase(getModule()), "global_config.rb").toString(); + } + + public void render() { + write(writer -> { + writer + .preamble() + .includeRequires() + .addModule(settings.getModule()) + .addModule("Plugins") + .writeDocstring(""" + GlobalConfig plugin sets default values for the {Client}'s config. + See {Hearth#config=} for setting default values. + """) + .openBlock("class GlobalConfig") + .write("") + .call(() -> renderCallMethod(writer)) + .write("") + .closeBlock("end") + .closeAllModules(); + }); + LOGGER.fine("Wrote config defaults plugin to " + rbFile()); + } + + private void renderCallMethod(RubyCodeWriter writer) { + writer + .openBlock("def call(config)") + .write("options = config.options") + .openBlock("$T.config.each do |key, value|", Hearth.HEARTH) + .write("config[key] = value unless options.key?(key)") + .closeBlock("end") + .closeBlock("end"); + } +} diff --git a/hearth/.rspec b/hearth/.rspec index c99d2e739..82b8369c1 100644 --- a/hearth/.rspec +++ b/hearth/.rspec @@ -1 +1 @@ ---require spec_helper +--require spec_helper \ No newline at end of file diff --git a/hearth/.rubocop.yml b/hearth/.rubocop.yml index 082ce0d1d..008329f6e 100644 --- a/hearth/.rubocop.yml +++ b/hearth/.rubocop.yml @@ -61,6 +61,10 @@ Metrics/PerceivedComplexity: - 'lib/hearth/cbor/encoder.rb' - 'lib/hearth/cbor/decoder.rb' +Style/BlockComments: + Exclude: + - 'spec/spec_helper.rb' + Style/Documentation: Exclude: - 'spec/**/*.rb' diff --git a/hearth/lib/hearth.rb b/hearth/lib/hearth.rb index 94f476198..c05fa862b 100755 --- a/hearth/lib/hearth.rb +++ b/hearth/lib/hearth.rb @@ -53,6 +53,26 @@ require_relative 'hearth/waiters/waiter' require_relative 'hearth/xml' +# Hearth is a low-level Ruby component library for code generated clients using +# the Smithy modeling language. module Hearth VERSION = File.read(File.expand_path('../VERSION', __dir__)).strip + + # @api private + @config = {} + + class << self + # @return [Hash] Returns a hash of default configuration options shared + # by all constructed clients. + attr_reader :config + + # @param [Hash] config + def config=(config) + unless config.is_a?(Hash) + raise ArgumentError, 'configuration must be a hash' + end + + @config = config + end + end end diff --git a/hearth/lib/hearth/client.rb b/hearth/lib/hearth/client.rb index 2bcb8e85f..ace0590ea 100644 --- a/hearth/lib/hearth/client.rb +++ b/hearth/lib/hearth/client.rb @@ -39,6 +39,7 @@ def initialize_config(options, config_class) config.interceptors.concat(client_interceptors) config.validate! config.freeze + config end def operation_config(options) @@ -55,6 +56,7 @@ def operation_config(options) operation_plugins&.each { |p| p.call(config) } config.interceptors.concat(operation_interceptors) config.validate! + config.freeze config end diff --git a/hearth/lib/hearth/config/resolver.rb b/hearth/lib/hearth/config/resolver.rb index a0da067a0..9fa528c43 100644 --- a/hearth/lib/hearth/config/resolver.rb +++ b/hearth/lib/hearth/config/resolver.rb @@ -15,11 +15,10 @@ def self.resolve(config, options, defaults = {}) new(config).send(:resolve, options, defaults) end - def key(key) + def [](key) @options[key] = resolve_default(key) unless @options.key?(key) @options[key] end - alias [] key private @@ -32,7 +31,7 @@ def resolve(options, defaults) @options = options @defaults = defaults @config.members.each do |key| - @config[key] = key(key) + @config[key] = self[key] end end diff --git a/hearth/lib/hearth/configuration.rb b/hearth/lib/hearth/configuration.rb index ad7989ea3..3df757abc 100644 --- a/hearth/lib/hearth/configuration.rb +++ b/hearth/lib/hearth/configuration.rb @@ -4,10 +4,14 @@ module Hearth # A module mixed into Config structs that resolves default value providers. module Configuration def initialize(**options) + @options = options.dup Hearth::Config::Resolver.resolve(self, options, _defaults) super end + # @return [Hash] The original configuration options. + attr_reader :options + def merge(configuration) self.class.new(**to_h.merge(configuration.to_h)) end diff --git a/hearth/sig/lib/hearth.rbs b/hearth/sig/lib/hearth.rbs new file mode 100644 index 000000000..e3bfffc81 --- /dev/null +++ b/hearth/sig/lib/hearth.rbs @@ -0,0 +1,7 @@ +module Hearth + VERSION: String + + attr_reader self.config: Hash[Symbol, untyped] + + def self.config=: (Hash[Symbol, untyped] config) -> Hash[Symbol, untyped] +end diff --git a/hearth/sig/lib/hearth/configuration.rbs b/hearth/sig/lib/hearth/configuration.rbs index 5170a818f..6e537a36d 100644 --- a/hearth/sig/lib/hearth/configuration.rbs +++ b/hearth/sig/lib/hearth/configuration.rbs @@ -2,6 +2,8 @@ module Hearth module Configuration[ServiceConfig] def initialize: (**Hash[Symbol, untyped] options) -> void + attr_reader options: Hash[Symbol, untyped] + def merge: (Hash[Symbol, untyped] configuration) -> ServiceConfig end end diff --git a/hearth/spec/hearth/cbor/cbor_spec.rb b/hearth/spec/hearth/cbor/cbor_spec.rb index d5a65188f..14f104429 100644 --- a/hearth/spec/hearth/cbor/cbor_spec.rb +++ b/hearth/spec/hearth/cbor/cbor_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative '../../spec_helper' - module Hearth describe CBOR do context 'decode success tests' do diff --git a/hearth/spec/hearth/cbor/decoder_spec.rb b/hearth/spec/hearth/cbor/decoder_spec.rb index 96bdf24f9..b6dd61c44 100644 --- a/hearth/spec/hearth/cbor/decoder_spec.rb +++ b/hearth/spec/hearth/cbor/decoder_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative '../../spec_helper' - module Hearth module CBOR # covers cases not included in test suite from cbor_spec diff --git a/hearth/spec/hearth/cbor/encoder_spec.rb b/hearth/spec/hearth/cbor/encoder_spec.rb index f46a3ccae..fde5141aa 100644 --- a/hearth/spec/hearth/cbor/encoder_spec.rb +++ b/hearth/spec/hearth/cbor/encoder_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative '../../spec_helper' - module Hearth module CBOR describe Encoder do diff --git a/hearth/spec/client_spec.rb b/hearth/spec/hearth/client_spec.rb similarity index 97% rename from hearth/spec/client_spec.rb rename to hearth/spec/hearth/client_spec.rb index 85d0d7bae..90c12327d 100644 --- a/hearth/spec/client_spec.rb +++ b/hearth/spec/hearth/client_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative 'spec_helper' - module Hearth module Test Config = Struct.new( @@ -136,6 +134,10 @@ def read_before_signing(*args); end config = subject.operation({}, interceptors: [interceptor]) expect(config.interceptors.to_a).to eq([interceptor]) end + + it 'freezes config' do + expect(subject.operation.frozen?).to be(true) + end end describe '#output_stream' do diff --git a/hearth/spec/hearth_spec.rb b/hearth/spec/hearth_spec.rb new file mode 100644 index 000000000..6ab44ce8c --- /dev/null +++ b/hearth/spec/hearth_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +describe Hearth do + it 'has a VERSION' do + expect(Hearth::VERSION).to be_a(String) + end + + describe '.config=' do + it 'raises when not given a hash' do + expect { Hearth.config = nil }.to raise_error(ArgumentError, /hash/) + end + + it 'sets the config' do + Hearth.config = { foo: 'bar' } + expect(Hearth.config[:foo]).to eq('bar') + end + end +end diff --git a/hearth/spec/spec_helper.rb b/hearth/spec/spec_helper.rb index fbb55f642..0294a2bf1 100644 --- a/hearth/spec/spec_helper.rb +++ b/hearth/spec/spec_helper.rb @@ -22,7 +22,8 @@ # the additional setup, and require it from the spec files that actually need # it. # -# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration + RSpec.configure do |config| # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest @@ -54,55 +55,55 @@ # triggering implicit auto-inclusion in groups with matching metadata. config.shared_context_metadata_behavior = :apply_to_host_groups - # The settings below are suggested to provide a good initial experience - # with RSpec, but feel free to customize to your heart's content. - # # This allows you to limit a spec run to individual examples or groups - # # you care about by tagging them with `:focus` metadata. When nothing - # # is tagged with `:focus`, all examples get run. RSpec also provides - # # aliases for `it`, `describe`, and `context` that include `:focus` - # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. - # config.filter_run_when_matching :focus - # - # # Allows RSpec to persist some state between runs in order to support - # # the `--only-failures` and `--next-failure` CLI options. We recommend - # # you configure your source control system to ignore this file. - # config.example_status_persistence_file_path = "spec/examples.txt" - # - # # Limits the available syntax to the non-monkey patched syntax that is - # # recommended. For more details, see: - # # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ - # # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ - # # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode - # config.disable_monkey_patching! - # - # # This setting enables warnings. It's recommended, but in some cases may - # # be too noisy due to issues in dependencies. - # config.warnings = true - # - # # Many RSpec users commonly either run the entire suite or an individual - # # file, and it's useful to allow more verbose output when running an - # # individual spec file. - # if config.files_to_run.one? - # # Use the documentation formatter for detailed output, - # # unless a formatter has already been configured - # # (e.g. via a command-line flag). - # config.default_formatter = "doc" - # end - # - # # Print the 10 slowest examples and example groups at the - # # end of the spec run, to help surface which specs are running - # # particularly slow. - # config.profile_examples = 10 - # - # # Run specs in random order to surface order dependencies. If you find an - # # order dependency and want to debug it, you can fix the order by - # # providing the seed, which is printed after each run. - # # --seed 1234 - # config.order = :random - # - # # Seed global randomization in this process using the `--seed` CLI option. - # # Setting this allows you to use `--seed` to deterministically reproduce - # # test failures related to randomization by passing the same `--seed` - # # value as the one that triggered the failure. - # Kernel.srand config.seed +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ + config.disable_monkey_patching! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end end diff --git a/tasks/test.rake b/tasks/test.rake index 696919453..b7baafdda 100644 --- a/tasks/test.rake +++ b/tasks/test.rake @@ -3,7 +3,7 @@ namespace :test do desc 'Run specs in hearth' task 'hearth' do - sh('bundle exec rspec hearth/spec -I hearth/lib') + sh('bundle exec rspec hearth/spec -I hearth/lib -r ./hearth/spec/spec_helper.rb') end desc 'Run generated and hand written specs for the white_label sdk' @@ -24,15 +24,14 @@ namespace :test do desc 'Run generated tests taken from smithy (endpoint specs)' task 'smithy-core-endpoint-tests' do build_dir = 'codegen/smithy-ruby-codegen-test/build/smithyprojections/smithy-ruby-codegen-test' - - test_sdk_dirs = Dir.glob("#{build_dir}/*/ruby-codegen/*") - .select { |d| !d.include?('white_label') && !d.include?('rpcv2cbor') && Dir.exist?("#{d}/spec") } - - specs = test_sdk_dirs.map { |d| "#{d}/spec" }.join(' ') - includes = test_sdk_dirs.map { |d| - "-I #{d}/lib" - }.join(' ') + ' -I hearth/lib' - - sh("bundle exec rspec #{specs} #{includes}") + test_sdk_dirs = + Dir.glob("#{build_dir}/*/ruby-codegen/*").select do |dir| + !dir.include?('white_label') && + !dir.include?('rails_json') && + !dir.include?('rpcv2cbor') + end + specs = test_sdk_dirs.map { |dir| "#{dir}/spec" }.join(' ') + includes = test_sdk_dirs.map { |dir| "-I #{dir}/lib" }.join(' ') + sh("bundle exec rspec #{specs} #{includes} -I hearth/lib") end end