From 5c7775e0ccfdd87b490fa9e44e7a4d34259c79ef Mon Sep 17 00:00:00 2001 From: Matt Muller <53055821+mullermp@users.noreply.github.com> Date: Fri, 7 Feb 2025 13:11:13 -0800 Subject: [PATCH] ClientHelper / SpecHelper improvement with source generation tests (#267) --- Rakefile | 15 +- gems/smithy/lib/smithy/generators/schema.rb | 1 + gems/smithy/lib/smithy/views/client/client.rb | 8 +- .../client/client_operation_example_spec.rb | 12 +- .../spec/interfaces/client/client_spec.rb | 42 +- .../client/client_syntax_example_spec.rb | 22 +- .../interfaces/client/customizations_spec.rb | 34 +- .../client/endpoint_parameters_spec.rb | 114 ++--- .../client/endpoint_provider_spec.rb | 40 +- .../spec/interfaces/client/errors_spec.rb | 180 ++++---- .../spec/interfaces/client/gemspec_spec.rb | 62 +-- .../spec/interfaces/client/module_spec.rb | 48 +-- .../spec/interfaces/client/shapes_spec.rb | 404 +----------------- .../spec/interfaces/client/types_spec.rb | 42 +- .../interfaces/schema/customizations_spec.rb | 5 + .../spec/interfaces/schema/gemspec_spec.rb | 5 + .../spec/interfaces/schema/module_spec.rb | 11 + .../spec/interfaces/schema/shapes_spec.rb | 11 + .../spec/interfaces/schema/types_spec.rb | 11 + gems/smithy/spec/interfaces/weld_spec.rb | 59 ++- gems/smithy/spec/spec_helper.rb | 88 ++-- gems/smithy/spec/support/client_helper.rb | 182 ++++++++ .../contexts/generated_client_context.rb | 21 + .../contexts/generated_schema_context.rb | 21 + .../examples/customizations_examples.rb | 31 ++ .../spec/support/examples/gemspec_examples.rb | 61 +++ .../spec/support/examples/module_examples.rb | 29 ++ .../spec/support/examples/shapes_examples.rb | 399 +++++++++++++++++ .../spec/support/examples/types_examples.rb | 33 ++ .../be_in_documentation_matcher.rb | 0 gems/smithy/spec/support/rbs_spy_test.rb | 8 +- 31 files changed, 1127 insertions(+), 872 deletions(-) create mode 100644 gems/smithy/spec/interfaces/schema/customizations_spec.rb create mode 100644 gems/smithy/spec/interfaces/schema/gemspec_spec.rb create mode 100644 gems/smithy/spec/interfaces/schema/module_spec.rb create mode 100644 gems/smithy/spec/interfaces/schema/shapes_spec.rb create mode 100644 gems/smithy/spec/interfaces/schema/types_spec.rb create mode 100644 gems/smithy/spec/support/client_helper.rb create mode 100644 gems/smithy/spec/support/contexts/generated_client_context.rb create mode 100644 gems/smithy/spec/support/contexts/generated_schema_context.rb create mode 100644 gems/smithy/spec/support/examples/customizations_examples.rb create mode 100644 gems/smithy/spec/support/examples/gemspec_examples.rb create mode 100644 gems/smithy/spec/support/examples/module_examples.rb create mode 100644 gems/smithy/spec/support/examples/shapes_examples.rb create mode 100644 gems/smithy/spec/support/examples/types_examples.rb rename gems/smithy/spec/support/{ => matchers}/be_in_documentation_matcher.rb (100%) diff --git a/Rakefile b/Rakefile index 0e5afb670..4f9e84a3d 100644 --- a/Rakefile +++ b/Rakefile @@ -18,14 +18,15 @@ namespace :smithy do spec_paths = [] include_paths = [] - tmp_dirs = [] + plans = [] rbs_targets = %w[Smithy Smithy::* Smithy::Client] sig_paths = ['gems/smithy-client/sig'] Dir.glob('gems/smithy/spec/fixtures/endpoints/*/model.json') do |model_path| test_name = model_path.split('/')[-2] test_module = test_name.gsub('-', '').camelize - tmpdir = SpecHelper.generate([test_module], :client, { fixture: "endpoints/#{test_name}" }) - tmp_dirs << [test_module.to_sym, tmpdir] + plan = SpecHelper.generate_gem(:client, fixture: "endpoints/#{test_name}", module_name: test_module) + plans << plan + tmpdir = plan.destination_root spec_paths << "#{tmpdir}/spec" include_paths << "#{tmpdir}/lib" include_paths << "#{tmpdir}/spec" @@ -51,9 +52,7 @@ namespace :smithy do sh(env, "bundle exec rspec #{specs} #{includes}") ensure - tmp_dirs.each do |name, tmpdir| - SpecHelper.cleanup([name], tmpdir) - end + plans.each { |plan| SpecHelper.cleanup_gem(plan) } end task 'spec' => %w[spec:unit spec:endpoints] @@ -96,8 +95,8 @@ namespace :smithy do task('smithy:spec:endpoints').invoke('rbs_test') end - desc 'Run RBS spy tests for unit tests and genreated endpoint provider specs.' - task 'rbs' => ['rbs:unit', 'rbs:endpoints'] + desc 'Run RBS spy tests for unit tests and generated endpoint provider specs.' + task 'rbs' => %w[rbs:unit rbs:endpoints] end namespace 'smithy-client' do diff --git a/gems/smithy/lib/smithy/generators/schema.rb b/gems/smithy/lib/smithy/generators/schema.rb index 930f0e01f..d05196f82 100644 --- a/gems/smithy/lib/smithy/generators/schema.rb +++ b/gems/smithy/lib/smithy/generators/schema.rb @@ -33,6 +33,7 @@ def gem_files source_files.each { |file, content| e.yield file, content } e.yield "lib/#{@gem_name}/customizations.rb", Views::Client::Customizations.new.render + rbs_files.each { |file, content| e.yield file, content } end end diff --git a/gems/smithy/lib/smithy/views/client/client.rb b/gems/smithy/lib/smithy/views/client/client.rb index 987b5f806..68e96f5e4 100644 --- a/gems/smithy/lib/smithy/views/client/client.rb +++ b/gems/smithy/lib/smithy/views/client/client.rb @@ -25,9 +25,13 @@ def gem_version end def require_plugins - @plugins.map do |plugin| - "require#{'_relative' if plugin.require_relative?} '#{plugin.require_path}'" + requires = [] + @plugins.each do |plugin| + next if !@plan.destination_root && plugin.require_relative? + + requires << "require#{'_relative' if plugin.require_relative?} '#{plugin.require_path}'" end + requires end def add_plugins diff --git a/gems/smithy/spec/interfaces/client/client_operation_example_spec.rb b/gems/smithy/spec/interfaces/client/client_operation_example_spec.rb index e93787727..f5aeffe50 100644 --- a/gems/smithy/spec/interfaces/client/client_operation_example_spec.rb +++ b/gems/smithy/spec/interfaces/client/client_operation_example_spec.rb @@ -1,13 +1,7 @@ # frozen_string_literal: true -describe 'Component: Client: Request/Response Syntax Examples' do - before(:all) do - @tmpdir = SpecHelper.generate(['ExamplesTrait'], :client) - end - - after(:all) do - SpecHelper.cleanup(['ExamplesTrait'], @tmpdir) - end +describe 'Client: Request/Response Syntax Examples' do + include_context 'generated client gem', fixture: 'examples_trait' it 'generates operation examples' do expected = <<~EXAMPLE @@ -77,7 +71,7 @@ } end EXAMPLE - client_file = File.join(@tmpdir, 'lib', 'examples_trait', 'client.rb') + client_file = File.join(@plan.destination_root, 'lib', 'examples_trait', 'client.rb') expect(expected).to be_in_documentation(client_file, 'ExamplesTrait::Client', 'operation') end end diff --git a/gems/smithy/spec/interfaces/client/client_spec.rb b/gems/smithy/spec/interfaces/client/client_spec.rb index d7df0c048..eb19f1bd6 100644 --- a/gems/smithy/spec/interfaces/client/client_spec.rb +++ b/gems/smithy/spec/interfaces/client/client_spec.rb @@ -1,30 +1,30 @@ # frozen_string_literal: true -describe 'Component: Client', rbs_test: true do - before(:all) do - @tmpdir = SpecHelper.generate(['Weather'], :client) - end +describe 'Client: Client' do + ['generated client gem', 'generated client from source code'].each do |context| + next if ENV['SMITHY_RUBY_RBS_TEST'] && context != 'generated client gem' - after(:all) do - SpecHelper.cleanup(['Weather'], @tmpdir) - end + context context do + include_context context, fixture: 'weather' - subject { Weather::Client.new } + subject { Weather::Client.new(endpoint: 'https://example.com') } - # it 'adds the HTTP plugin' do - # expect(Weather::Client.plugins).to include(Smithy::Client::Plugins::NetHTTP) - # end + it 'loads plugins' do + expect(Weather::Client.plugins).to include(Smithy::Client::Plugins::NetHTTP) + end - it 'has operation methods' do - expect(subject).to respond_to(:get_city, :get_current_time, :get_forecast, :list_cities) - end + it 'has operation methods' do + expect(subject).to respond_to(:get_city, :get_current_time, :get_forecast, :list_cities) + end - it 'builds input for operations' do - input = subject.send(:build_input, :get_city, { id: 1 }) - expect(input).to be_a(Smithy::Client::Input) - end + it 'builds input for operations' do + input = subject.send(:build_input, :get_city, { id: 1 }) + expect(input).to be_a(Smithy::Client::Input) + end - # it 'can call operations' do - # subject.get_city(id: 1) - # end + # it 'can call operations' do + # subject.get_city(city_id: '1') + # end + end + end end diff --git a/gems/smithy/spec/interfaces/client/client_syntax_example_spec.rb b/gems/smithy/spec/interfaces/client/client_syntax_example_spec.rb index 5d9580388..2a4271bfc 100644 --- a/gems/smithy/spec/interfaces/client/client_syntax_example_spec.rb +++ b/gems/smithy/spec/interfaces/client/client_syntax_example_spec.rb @@ -1,13 +1,7 @@ # frozen_string_literal: true -describe 'Component: Client: Request/Response Syntax Examples' do - before(:all) do - @tmpdir = SpecHelper.generate(['SyntaxExamples'], :client) - end - - after(:all) do - SpecHelper.cleanup(['SyntaxExamples'], @tmpdir) - end +describe 'Client: Client Request/Response Syntax Examples' do + include_context 'generated client gem', fixture: 'syntax_examples' it 'generates request and response syntax examples' do expected = <<~EXAMPLE @@ -129,18 +123,12 @@ } } EXAMPLE - client_file = File.join(@tmpdir, 'lib', 'syntax_examples', 'client.rb') + client_file = File.join(@plan.destination_root, 'lib', 'syntax_examples', 'client.rb') expect(expected).to be_in_documentation(client_file, 'SyntaxExamples::Client', 'operation') end context 'recursive shapes' do - before(:all) do - @tmpdir = SpecHelper.generate(['Recursive'], :client) - end - - after(:all) do - SpecHelper.cleanup(['Recursive'], @tmpdir) - end + include_context 'generated client gem', fixture: 'recursive' it 'handles recursive shapes' do expected = <<~EXAMPLE @@ -164,7 +152,7 @@ } } EXAMPLE - client_file = File.join(@tmpdir, 'lib', 'recursive', 'client.rb') + client_file = File.join(@plan.destination_root, 'lib', 'recursive', 'client.rb') expect(expected).to be_in_documentation(client_file, 'Recursive::Client', 'operation') end end diff --git a/gems/smithy/spec/interfaces/client/customizations_spec.rb b/gems/smithy/spec/interfaces/client/customizations_spec.rb index bbb81cc11..ab8ed270a 100644 --- a/gems/smithy/spec/interfaces/client/customizations_spec.rb +++ b/gems/smithy/spec/interfaces/client/customizations_spec.rb @@ -1,35 +1,5 @@ # frozen_string_literal: true -describe 'Component: Customizations' do - before(:all) do - @tmpdir = SpecHelper.generate(['Weather'], :client) - end - - after(:all) do - SpecHelper.cleanup(['Weather'], @tmpdir) - end - - subject { Weather::Client.new(endpoint: 'https://example.com') } - - it 'should have a customizations file' do - expect(File).to exist(File.join(@tmpdir, 'lib', 'weather', 'customizations.rb')) - end - - it 'should require the customizations file' do - expect(require('weather/customizations')).to eq(false) - end - - it 'does not overwrite an existing customizations file' do - customization = <<~RUBY - module Weather - # @api private - module Customizations; end - end - RUBY - customizations_file = File.join(@tmpdir, 'lib', 'weather', 'customizations.rb') - expect(File.read(customizations_file)).to_not include(customization) - File.write(customizations_file, customization) - SpecHelper.generate(['Weather'], :client, destination_root: @tmpdir) - expect(File.read(customizations_file)).to include(customization) - end +describe 'Client: Customizations' do + include_examples 'customizations', 'generated client gem' end diff --git a/gems/smithy/spec/interfaces/client/endpoint_parameters_spec.rb b/gems/smithy/spec/interfaces/client/endpoint_parameters_spec.rb index 3c860ae56..92c609233 100644 --- a/gems/smithy/spec/interfaces/client/endpoint_parameters_spec.rb +++ b/gems/smithy/spec/interfaces/client/endpoint_parameters_spec.rb @@ -1,71 +1,71 @@ # frozen_string_literal: true -describe 'Component: EndpointParameters', rbs_test: true do - before(:all) do - @tmpdir = SpecHelper.generate(['EndpointBindings'], :client, fixture: 'endpoints/endpoint-bindings') - end +describe 'Client: EndpointParameters', rbs_test: true do + ['generated client gem', 'generated client from source code'].each do |context| + next if ENV['SMITHY_RUBY_RBS_TEST'] && context != 'generated client gem' - after(:all) do - SpecHelper.cleanup(['EndpointBindings'], @tmpdir) - end + context context do + include_context context, fixture: 'endpoints/endpoint-bindings', module_name: 'EndpointBindings' - subject { EndpointBindings::EndpointParameters.new } + subject { EndpointBindings::EndpointParameters.new } - describe '#initialize' do - it 'initializes with default values' do - expect(subject.baz).to eq('baz') - expect(subject.boolean_param).to eq(true) - expect(subject.endpoint).to be_nil - expect(subject.bar).to be_nil - end - end + describe '#initialize' do + it 'initializes with default values' do + expect(subject.baz).to eq('baz') + expect(subject.boolean_param).to eq(true) + expect(subject.endpoint).to be_nil + expect(subject.bar).to be_nil + end + end - describe '.create' do - let(:config) do - client = EndpointBindings::Client.new(bar: 'config_bar', endpoint: 'config_endpoint', boolean_param: false) - client.config - end + describe '.create' do + let(:config) do + client = EndpointBindings::Client.new(bar: 'config_bar', endpoint: 'config_endpoint', boolean_param: false) + client.config + end - context 'no_bindings_operation' do - it 'creates with default values and values from config' do - context = double(config: config, operation_name: :no_bindings_operation, params: {}) - parameters = EndpointBindings::EndpointParameters.create(context) - expect(parameters.baz).to eq('baz') - expect(parameters.endpoint).to eq('config_endpoint') - expect(parameters.bar).to eq('config_bar') - expect(parameters.boolean_param).to eq(false) - end - end + context 'no_bindings_operation' do + it 'creates with default values and values from config' do + context = double(config: config, operation_name: :no_bindings_operation, params: {}) + parameters = EndpointBindings::EndpointParameters.create(context) + expect(parameters.baz).to eq('baz') + expect(parameters.endpoint).to eq('config_endpoint') + expect(parameters.bar).to eq('config_bar') + expect(parameters.boolean_param).to eq(false) + end + end - context 'static_context_operation' do - it 'creates with values from StaticContextParams' do - context = double(config: config, operation_name: :static_context_operation, params: {}) - parameters = EndpointBindings::EndpointParameters.create(context) - expect(parameters.bar).to eq('static-context') - end - end + context 'static_context_operation' do + it 'creates with values from StaticContextParams' do + context = double(config: config, operation_name: :static_context_operation, params: {}) + parameters = EndpointBindings::EndpointParameters.create(context) + expect(parameters.bar).to eq('static-context') + end + end - context 'context_params_operation' do - it 'creates with values from operation params' do - params = { bar: 'operation-bar' } - context = double(config: config, operation_name: :context_params_operation, params: params) - parameters = EndpointBindings::EndpointParameters.create(context) - expect(parameters.bar).to eq('operation-bar') - end - end + context 'context_params_operation' do + it 'creates with values from operation params' do + params = { bar: 'operation-bar' } + context = double(config: config, operation_name: :context_params_operation, params: params) + parameters = EndpointBindings::EndpointParameters.create(context) + expect(parameters.bar).to eq('operation-bar') + end + end - context 'operation_context_params_operation' do - it 'creates with values from operation params' do - params = { - nested: { bar: 'nested-bar', baz: 'nested-baz' }, - boolean_param: false - } - context = double(config: config, operation_name: :operation_context_params_operation, params: params) + context 'operation_context_params_operation' do + it 'creates with values from operation params' do + params = { + nested: { bar: 'nested-bar', baz: 'nested-baz' }, + boolean_param: false + } + context = double(config: config, operation_name: :operation_context_params_operation, params: params) - parameters = EndpointBindings::EndpointParameters.create(context) - expect(parameters.bar).to eq('nested-bar') - expect(parameters.baz).to eq('nested-baz') - expect(parameters.boolean_param).to eq(false) + parameters = EndpointBindings::EndpointParameters.create(context) + expect(parameters.bar).to eq('nested-bar') + expect(parameters.baz).to eq('nested-baz') + expect(parameters.boolean_param).to eq(false) + end + end end end end diff --git a/gems/smithy/spec/interfaces/client/endpoint_provider_spec.rb b/gems/smithy/spec/interfaces/client/endpoint_provider_spec.rb index 52ebb6eea..3f211e105 100644 --- a/gems/smithy/spec/interfaces/client/endpoint_provider_spec.rb +++ b/gems/smithy/spec/interfaces/client/endpoint_provider_spec.rb @@ -1,30 +1,30 @@ # frozen_string_literal: true -describe 'Component: EndpointProvider', rbs_test: true do - before(:all) do - @tmpdir = SpecHelper.generate(['EndpointDefaults'], :client, fixture: 'endpoints/default-values') - end +describe 'Client: EndpointProvider', rbs_test: true do + ['generated client gem', 'generated client from source code'].each do |context| + next if ENV['SMITHY_RUBY_RBS_TEST'] && context != 'generated client gem' - after(:all) do - SpecHelper.cleanup(['EndpointDefaults'], @tmpdir) - end + context context do + include_context context, fixture: 'endpoints/default-values', module_name: 'EndpointDefaults' - subject { EndpointDefaults::EndpointProvider.new } + subject { EndpointDefaults::EndpointProvider.new } - describe '.resolve_endpoint' do - it 'resolves the endpoint' do - params = EndpointDefaults::EndpointParameters.new(bar: 'bar', baz: 'baz') + describe '.resolve_endpoint' do + it 'resolves the endpoint' do + params = EndpointDefaults::EndpointParameters.new(bar: 'bar', baz: 'baz') - out = subject.resolve_endpoint(params) - expect(out).to be_a(Smithy::Client::EndpointRules::Endpoint) - expect(out.uri).to eq('https://example.com/baz') - end + out = subject.resolve_endpoint(params) + expect(out).to be_a(Smithy::Client::EndpointRules::Endpoint) + expect(out.uri).to eq('https://example.com/baz') + end - it 'raises errors from rules' do - params = EndpointDefaults::EndpointParameters.new(bar: nil, baz: 'baz') - expect do - subject.resolve_endpoint(params) - end.to raise_error(ArgumentError, 'endpoint error') + it 'raises errors from rules' do + params = EndpointDefaults::EndpointParameters.new(bar: nil, baz: 'baz') + expect do + subject.resolve_endpoint(params) + end.to raise_error(ArgumentError, 'endpoint error') + end + end end end end diff --git a/gems/smithy/spec/interfaces/client/errors_spec.rb b/gems/smithy/spec/interfaces/client/errors_spec.rb index 07a72137a..b3b11ed94 100644 --- a/gems/smithy/spec/interfaces/client/errors_spec.rb +++ b/gems/smithy/spec/interfaces/client/errors_spec.rb @@ -1,94 +1,94 @@ # frozen_string_literal: true -describe 'Component: Errors', rbs_test: true do - before(:all) do - @tmpdir = SpecHelper.generate(['Errors'], :client) - end - - after(:all) do - SpecHelper.cleanup(['Errors'], @tmpdir) - end - - it 'generates an errors module' do - expect(Errors::Errors).to be_a(Module) - end - - it 'generates client errors' do - expect(defined?(Errors::Errors::ClientError)).to_not be_nil - end - - it 'generates client retryable errors' do - error = Errors::Errors::ClientRetryableError.new(nil, nil) - expect(error.retryable?).to be(true) - end - - it 'generates client throttling errors' do - error = Errors::Errors::ClientThrottlingError.new(nil, nil) - expect(error.retryable?).to be(true) - expect(error.throttling?).to be(true) - end - - it 'generates server errors' do - expect(defined?(Errors::Errors::ServerError)).to_not be_nil - end - - it 'generates server retryable errors' do - error = Errors::Errors::ServerRetryableError.new(nil, nil) - expect(error.retryable?).to be(true) - end - - it 'generates server throttling errors' do - error = Errors::Errors::ServerThrottlingError.new(nil, nil) - expect(error.retryable?).to be(true) - expect(error.throttling?).to be(true) - end - - it 'generates errors from the service shape' do - expect(defined?(Errors::Errors::ServiceError)).to_not be_nil - end - - it 'generates errors with messages from data' do - data = Errors::Types::ServiceError.new(message: 'message') - error = Errors::Errors::ServiceError.new(nil, nil, data) - expect(error.message).to eq('message') - expect { raise error }.to raise_error(Errors::Errors::ServiceError, 'message') - end - - it 'allows overriding the message' do - data = Errors::Types::ServiceError.new(message: 'message') - error = Errors::Errors::ServiceError.new(nil, 'new message', data) - expect(error.message).to eq('new message') - expect { raise error }.to raise_error(Errors::Errors::ServiceError, 'new message') - end - - it 'can return data members' do - structure = Errors::Types::Structure.new(value: 'foo') - data = Errors::Types::ServiceError.new(structure: structure) - error = Errors::Errors::ServiceError.new(nil, nil, data) - expect(error.data.structure.value).to eq('foo') - end - - it 'generates a dynamic error class' do - expect(defined?(Errors::Errors::DynamicError)).to be nil - new_error = Errors::Errors::DynamicError.new(nil, nil) - expect(new_error).to be_a(Smithy::Client::Errors::ServiceError) - end - - it 'generates documentation for error classes' do - errors_file = File.join(@tmpdir, 'lib', 'errors', 'errors.rb') - expected = <<~DOC - This is a service error. - It is raised sometimes. - DOC - expect(expected).to be_in_documentation(errors_file, 'Errors::Errors::ServiceError') - end - - it 'generates documentation for error members' do - errors_file = File.join(@tmpdir, 'lib', 'errors', 'errors.rb') - expected = <<~DOC - This is a structure in a service error. - It sometimes has data. - DOC - expect(expected).to be_in_documentation(errors_file, 'Errors::Errors::ServiceError', 'structure') +describe 'Client: Errors', rbs_test: true do + ['generated client gem', 'generated client from source code'].each do |context| + next if ENV['SMITHY_RUBY_RBS_TEST'] && context != 'generated client gem' + + context context do + include_context 'generated client gem', fixture: 'errors' + + it 'generates an errors module' do + expect(Errors::Errors).to be_a(Module) + end + + it 'generates client errors' do + expect(defined?(Errors::Errors::ClientError)).to_not be_nil + end + + it 'generates client retryable errors' do + error = Errors::Errors::ClientRetryableError.new(nil, nil) + expect(error.retryable?).to be(true) + end + + it 'generates client throttling errors' do + error = Errors::Errors::ClientThrottlingError.new(nil, nil) + expect(error.retryable?).to be(true) + expect(error.throttling?).to be(true) + end + + it 'generates server errors' do + expect(defined?(Errors::Errors::ServerError)).to_not be_nil + end + + it 'generates server retryable errors' do + error = Errors::Errors::ServerRetryableError.new(nil, nil) + expect(error.retryable?).to be(true) + end + + it 'generates server throttling errors' do + error = Errors::Errors::ServerThrottlingError.new(nil, nil) + expect(error.retryable?).to be(true) + expect(error.throttling?).to be(true) + end + + it 'generates errors from the service shape' do + expect(defined?(Errors::Errors::ServiceError)).to_not be_nil + end + + it 'generates errors with messages from data' do + data = Errors::Types::ServiceError.new(message: 'message') + error = Errors::Errors::ServiceError.new(nil, nil, data) + expect(error.message).to eq('message') + expect { raise error }.to raise_error(Errors::Errors::ServiceError, 'message') + end + + it 'allows overriding the message' do + data = Errors::Types::ServiceError.new(message: 'message') + error = Errors::Errors::ServiceError.new(nil, 'new message', data) + expect(error.message).to eq('new message') + expect { raise error }.to raise_error(Errors::Errors::ServiceError, 'new message') + end + + it 'can return data members' do + structure = Errors::Types::Structure.new(value: 'foo') + data = Errors::Types::ServiceError.new(structure: structure) + error = Errors::Errors::ServiceError.new(nil, nil, data) + expect(error.data.structure.value).to eq('foo') + end + + it 'generates a dynamic error class' do + expect(defined?(Errors::Errors::DynamicError)).to be nil + new_error = Errors::Errors::DynamicError.new(nil, nil) + expect(new_error).to be_a(Smithy::Client::Errors::ServiceError) + end + + it 'generates documentation for error classes' do + errors_file = File.join(@plan.destination_root, 'lib', 'errors', 'errors.rb') + expected = <<~DOC + This is a service error. + It is raised sometimes. + DOC + expect(expected).to be_in_documentation(errors_file, 'Errors::Errors::ServiceError') + end + + it 'generates documentation for error members' do + errors_file = File.join(@plan.destination_root, 'lib', 'errors', 'errors.rb') + expected = <<~DOC + This is a structure in a service error. + It sometimes has data. + DOC + expect(expected).to be_in_documentation(errors_file, 'Errors::Errors::ServiceError', 'structure') + end + end end end diff --git a/gems/smithy/spec/interfaces/client/gemspec_spec.rb b/gems/smithy/spec/interfaces/client/gemspec_spec.rb index 95eeed170..e939159f7 100644 --- a/gems/smithy/spec/interfaces/client/gemspec_spec.rb +++ b/gems/smithy/spec/interfaces/client/gemspec_spec.rb @@ -1,63 +1,5 @@ # frozen_string_literal: true -describe 'Component: Gemspec' do - %i[schema client].each do |plan_type| - context "#{plan_type} generator" do - context 'single module' do - before(:all) do - @tmpdir = SpecHelper.generate(['Weather'], plan_type) - end - - after(:all) do - SpecHelper.cleanup(['Weather'], @tmpdir) - end - - let(:gem_name) { "weather#{plan_type == :schema ? '-schema' : ''}" } - - it 'generates a gemspec with schema suffix' do - gemspec = File.join(@tmpdir, "#{gem_name}.gemspec") - expect(File.exist?(gemspec)).to be(true) - end - - it 'has a gem specification' do - gemspec = File.join(@tmpdir, "#{gem_name}.gemspec") - gem = Gem::Specification.load(gemspec) - expect(gem.name).to eq(gem_name) - expect(gem.version).to eq(Gem::Version.new('0.1.0')) - expect(gem.summary).to eq('Generated gem using Smithy') - expect(gem.authors).to eq(['Smithy Ruby']) - expect(gem.files).to include("lib/#{gem_name}/types.rb") - expect(gem.dependencies).to include(Gem::Dependency.new('smithy-client', '~> 1')) - end - end - - context 'nested module' do - before(:all) do - @tmpdir = SpecHelper.generate(%w[SomeOrganization Weather], plan_type, fixture: 'weather') - end - - after(:all) do - SpecHelper.cleanup(%w[SomeOrganization Weather], @tmpdir) - end - - let(:gem_name) { "some_organization-weather#{plan_type == :schema ? '-schema' : ''}" } - - it 'generates a gemspec with schema suffix' do - gemspec = File.join(@tmpdir, "#{gem_name}.gemspec") - expect(File.exist?(gemspec)).to be(true) - end - - it 'has a gem specification' do - gemspec = File.join(@tmpdir, "#{gem_name}.gemspec") - gem = Gem::Specification.load(gemspec) - expect(gem.name).to eq(gem_name) - expect(gem.version).to eq(Gem::Version.new('0.1.0')) - expect(gem.summary).to eq('Generated gem using Smithy') - expect(gem.authors).to eq(['Smithy Ruby']) - expect(gem.files).to include("lib/#{gem_name}/types.rb") - expect(gem.dependencies).to include(Gem::Dependency.new('smithy-client', '~> 1')) - end - end - end - end +describe 'Client: Gemspec' do + include_examples 'gemspec', 'generated client gem' end diff --git a/gems/smithy/spec/interfaces/client/module_spec.rb b/gems/smithy/spec/interfaces/client/module_spec.rb index 51c445c0b..46e9106af 100644 --- a/gems/smithy/spec/interfaces/client/module_spec.rb +++ b/gems/smithy/spec/interfaces/client/module_spec.rb @@ -1,48 +1,10 @@ # frozen_string_literal: true -describe 'Component: Module' do - %i[schema client].each do |plan_type| - context "#{plan_type} generator" do - context 'single module' do - before(:all) do - @tmpdir = SpecHelper.generate(['Weather'], plan_type) - end - - after(:all) do - SpecHelper.cleanup(['Weather'], @tmpdir) - end - - let(:gem_name) { "weather#{plan_type == :schema ? '-schema' : ''}" } - - it 'has a version' do - expect(Weather::VERSION).to eq('0.1.0') - end - - it 'requires interfaces' do - expect(Weather::Types).to be_a(Module) - expect(Weather::Shapes).to be_a(Module) - end - end - - context 'nested module' do - before(:all) do - @tmpdir = SpecHelper.generate(%w[SomeOrganization Weather], plan_type, fixture: 'weather') - end - - after(:all) do - SpecHelper.cleanup(%w[SomeOrganization Weather], @tmpdir) - end - - let(:gem_name) { "some_organization-weather#{plan_type == :schema ? '-schema' : ''}" } - - it 'has a version' do - expect(SomeOrganization::Weather::VERSION).to eq('0.1.0') - end - - it 'requires interfaces' do - expect(SomeOrganization::Weather::Types).to be_a(Module) - expect(SomeOrganization::Weather::Shapes).to be_a(Module) - end +describe 'Client: Module' do + context 'single module' do + ['generated client gem', 'generated client from source code'].each do |context| + context context do + include_examples 'gem module', context end end end diff --git a/gems/smithy/spec/interfaces/client/shapes_spec.rb b/gems/smithy/spec/interfaces/client/shapes_spec.rb index 458ae3094..6b6d124ff 100644 --- a/gems/smithy/spec/interfaces/client/shapes_spec.rb +++ b/gems/smithy/spec/interfaces/client/shapes_spec.rb @@ -1,405 +1,11 @@ # frozen_string_literal: true -describe 'Component: Shapes', rbs_test: true do - before(:all) do - @tmpdir = SpecHelper.generate(['ShapeService'], :client, fixture: 'shapes') - end - - after(:all) do - SpecHelper.cleanup(['ShapeService'], @tmpdir) - end - - let(:fixture) { JSON.load_file(File.expand_path('../../fixtures/shapes/model.json', __dir__.to_s)) } - - subject { ShapeService::Shapes } - - it 'generates a shapes module' do - expect(ShapeService::Shapes).to be_a(Module) - end - - def expect_generated_shape(subject, shape_class, shape_hash) - id, shape = shape_hash - expect(subject).to be_a(shape_class) - expect(subject.id).to eq(id) - expect(subject.traits).to eq(shape['traits']) - end - - context 'blob' do - subject { ShapeService::Shapes::Blob } - let(:shape_class) { Smithy::Client::Shapes::BlobShape } - let(:shape_hash) do - fixture['shapes'].find { |_, s| s['type'] == 'blob' } - end - - it 'generates a blob shape' do - expect_generated_shape(subject, shape_class, shape_hash) - end - end - - context 'boolean' do - subject { ShapeService::Shapes::Boolean } - let(:shape_class) { Smithy::Client::Shapes::BooleanShape } - let(:shape_hash) do - fixture['shapes'].find { |_, s| s['type'] == 'boolean' } - end - - it 'generates a boolean shape' do - expect_generated_shape(subject, shape_class, shape_hash) - end - end - - context 'string' do - subject { ShapeService::Shapes::String } - let(:shape_class) { Smithy::Client::Shapes::StringShape } - let(:shape_hash) do - fixture['shapes'].find { |_, s| s['type'] == 'string' } - end - - it 'generates a string shape' do - expect_generated_shape(subject, shape_class, shape_hash) - end - end - - context 'byte' do - subject { ShapeService::Shapes::Byte } - let(:shape_class) { Smithy::Client::Shapes::IntegerShape } - let(:shape_hash) do - fixture['shapes'].find { |_, s| s['type'] == 'byte' } - end - - it 'generates a byte shape' do - expect_generated_shape(subject, shape_class, shape_hash) - end - end - - context 'short' do - subject { ShapeService::Shapes::Short } - let(:shape_class) { Smithy::Client::Shapes::IntegerShape } - let(:shape_hash) do - fixture['shapes'].find { |_, s| s['type'] == 'short' } - end - - it 'generates a short shape' do - expect_generated_shape(subject, shape_class, shape_hash) - end - end - - context 'integer' do - subject { ShapeService::Shapes::Integer } - let(:shape_class) { Smithy::Client::Shapes::IntegerShape } - let(:shape_hash) do - fixture['shapes'].find { |_, s| s['type'] == 'integer' } - end - - it 'generates an integer shape' do - expect_generated_shape(subject, shape_class, shape_hash) - end - end - - context 'long' do - subject { ShapeService::Shapes::Long } - let(:shape_class) { Smithy::Client::Shapes::IntegerShape } - let(:shape_hash) do - fixture['shapes'].find { |_, s| s['type'] == 'long' } - end - - it 'generates a long shape' do - expect_generated_shape(subject, shape_class, shape_hash) - end - end - - context 'float' do - subject { ShapeService::Shapes::Float } - let(:shape_class) { Smithy::Client::Shapes::FloatShape } - let(:shape_hash) do - fixture['shapes'].find { |_, s| s['type'] == 'float' } - end - - it 'generates a float shape' do - expect_generated_shape(subject, shape_class, shape_hash) - end - end - - context 'double' do - subject { ShapeService::Shapes::Double } - let(:shape_class) { Smithy::Client::Shapes::FloatShape } - let(:shape_hash) do - fixture['shapes'].find { |_, s| s['type'] == 'double' } - end - - it 'generates a double shape' do - expect_generated_shape(subject, shape_class, shape_hash) - end - end - - context 'bigInteger' do - subject { ShapeService::Shapes::BigInteger } - let(:shape_class) { Smithy::Client::Shapes::IntegerShape } - let(:shape_hash) do - fixture['shapes'].find { |_, s| s['type'] == 'bigInteger' } - end - - it 'generates a big integer shape' do - expect_generated_shape(subject, shape_class, shape_hash) - end - end - - context 'bigDecimal' do - subject { ShapeService::Shapes::BigDecimal } - let(:shape_class) { Smithy::Client::Shapes::BigDecimalShape } - let(:shape_hash) do - fixture['shapes'].find { |_, s| s['type'] == 'bigDecimal' } - end - - it 'generates a big decimal shape' do - expect_generated_shape(subject, shape_class, shape_hash) - end - end - - context 'timestamp' do - subject { ShapeService::Shapes::Timestamp } - let(:shape_class) { Smithy::Client::Shapes::TimestampShape } - let(:shape_hash) do - fixture['shapes'].find { |_, s| s['type'] == 'timestamp' } - end - - it 'generates a timestamp shape' do - expect_generated_shape(subject, shape_class, shape_hash) - end - end - - context 'document' do - subject { ShapeService::Shapes::Document } - let(:shape_class) { Smithy::Client::Shapes::DocumentShape } - let(:shape_hash) do - fixture['shapes'].find { |_, s| s['type'] == 'document' } - end - - it 'generates a document shape' do - expect_generated_shape(subject, shape_class, shape_hash) - end - end - - context 'enum' do - subject { ShapeService::Shapes::Enum } - let(:shape_class) { Smithy::Client::Shapes::EnumShape } - let(:shape_hash) do - fixture['shapes'].find { |_, s| s['type'] == 'enum' } - end - let(:expected_member) do - _, shape = shape_hash - shape['members']['FOO'] - end - - it 'generates an enum shape' do - expect_generated_shape(subject, shape_class, shape_hash) - end - - it 'has members' do - expect(subject.members.keys).to eq(%i[foo]) - expect(subject.members[:foo]).to be_a(Smithy::Client::Shapes::MemberShape) - expect(subject.members[:foo].shape).to be_a(Smithy::Client::Shapes::StructureShape) - expect(subject.members[:foo].traits).to eq(expected_member['traits']) - expect(subject.members[:foo].shape.id).to eq(expected_member['target']) - end - - it 'has a member with traits' do - expect(subject.member(:foo).traits).to eq(expected_member['traits']) - end - end - - context 'intEnum' do - subject { ShapeService::Shapes::IntEnum } - let(:shape_class) { Smithy::Client::Shapes::IntEnumShape } - let(:shape_hash) do - fixture['shapes'].find { |_, s| s['type'] == 'intEnum' } - end - let(:expected_member) do - _, shape = shape_hash - shape['members']['BAZ'] - end - - it 'generates an int enum shape' do - expect_generated_shape(subject, shape_class, shape_hash) - end - - it 'has members' do - expect(subject.members.keys).to eq(%i[baz]) - expect(subject.members[:baz]).to be_a(Smithy::Client::Shapes::MemberShape) - expect(subject.members[:baz].shape).to be_a(Smithy::Client::Shapes::StructureShape) - expect(subject.members[:baz].traits).to eq(expected_member['traits']) - expect(subject.members[:baz].shape.id).to eq(expected_member['target']) - end - - it 'has a member with traits' do - expect(subject.member(:baz).traits).to eq(expected_member['traits']) - end - end - - context 'list' do - subject { ShapeService::Shapes::List } - let(:shape_class) { Smithy::Client::Shapes::ListShape } - let(:shape_hash) do - fixture['shapes'].find { |_, s| s['type'] == 'list' } - end - let(:expected_member) do - _, shape = shape_hash - shape['member'] - end - - it 'generates a list shape' do - expect_generated_shape(subject, shape_class, shape_hash) - end - - it 'has a member' do - expect(subject.member).to be_a(Smithy::Client::Shapes::MemberShape) - expect(subject.member.shape).to be_a(Smithy::Client::Shapes::StringShape) - expect(subject.member.shape.id).to eq(expected_member['target']) - end - - it 'has a member with traits' do - expect(subject.member.traits).to eq(expected_member['traits']) - end - end - - context 'map' do - subject { ShapeService::Shapes::Map } - let(:shape_class) { Smithy::Client::Shapes::MapShape } - let(:shape_hash) do - fixture['shapes'].find { |_, s| s['type'] == 'map' } - end - let(:expected_shape) do - _, shape = shape_hash - shape - end - - it 'generates a map shape' do - expect_generated_shape(subject, shape_class, shape_hash) - end - - it 'has key and value members' do - expect(subject.key).to be_a(Smithy::Client::Shapes::MemberShape) - expect(subject.key.shape).to be_a(Smithy::Client::Shapes::StringShape) - expect(subject.key.shape.id).to eq(expected_shape['key']['target']) - expect(subject.value).to be_a(Smithy::Client::Shapes::MemberShape) - expect(subject.value.shape).to be_a(Smithy::Client::Shapes::StringShape) - expect(subject.value.shape.id).to eq(expected_shape['value']['target']) - end - - it 'has keys and values with traits' do - expect(subject.key.traits).to eq(expected_shape['key']['traits']) - expect(subject.value.traits).to eq(expected_shape['value']['traits']) - end - end - - context 'union' do - subject { ShapeService::Shapes::Union } - let(:shape_class) { Smithy::Client::Shapes::UnionShape } - let(:shape_hash) do - fixture['shapes'].find { |_, s| s['type'] == 'union' } - end - let(:expected_shape) do - _, shape = shape_hash - shape - end - - it 'generates a union shape' do - expect_generated_shape(subject, shape_class, shape_hash) - end - - it 'has members' do - expected_members = expected_shape['members'].keys.map(&:to_sym) - expect(subject.members.keys).to eq(expected_members) - end - - it 'has a member with traits' do - expected_member = expected_shape['members'].slice('string').values.first - expect(subject.member(:string).traits).to eq(expected_member['traits']) - end - - it 'has a type' do - expect(subject.type).to eq(ShapeService::Types::Union) - end - - it 'has members with types' do - expect(subject.member(:string)).to be_a(Smithy::Client::Shapes::MemberShape) - expect(subject.member_type(:string)).to eq(ShapeService::Types::Union::String) - end - end - - context 'structure' do - subject { ShapeService::Shapes::Structure } - let(:shape_class) { Smithy::Client::Shapes::StructureShape } - let(:shape_hash) do - fixture['shapes'].find { |k, _| k.include?('Structure') } - end - let(:expected_shape) do - _, shape = shape_hash - shape - end - - it 'generates a structure shape' do - expect_generated_shape(subject, shape_class, shape_hash) - end - - it 'has members' do - expected_members = - expected_shape['members'] - .keys - .map { |m| m.underscore.to_sym } - expect(subject.members.keys).to eq(expected_members) - end - - it 'has a member with traits' do - expected_member = - expected_shape['members'] - .slice('member') - .values - .first - expect(subject.member(:member).traits).to eq(expected_member['traits']) - end - end - - context 'schema' do - it 'is a schema' do - expect(subject::SCHEMA).to be_a(Smithy::Client::Schema) - end - - context 'service' do - let(:service_shape) { subject::SCHEMA.service } - let(:expected_service) { fixture['shapes'].find { |_k, v| v['type'] == 'service' } } - - it 'is a service shape and able to access service shape data' do - expect(service_shape).to be_a(Smithy::Client::Shapes::ServiceShape) - expect(service_shape.id).to eql(expected_service[0]) - expect(service_shape.version).to eq(expected_service[1]['version']) - - if (expected_traits = expected_service[1]['traits']) - expect(service_shape.traits).to include(expected_traits) - end - end - end - - context 'operations' do - let(:operations) { subject::SCHEMA.operations } - let(:operation_shapes) { fixture.select { |_k, v| v['type'] == 'operation' } } - - it 'is not empty' do - expect(operations).not_to be_empty - end - - it 'made of operation shapes and able to access its contents' do - operation_shapes.each do |name, shape| - generated_shape = subject::SCHEMA.operation(name.underscore) +describe 'Client: Shapes', rbs_test: true do + ['generated client gem', 'generated client from source code'].each do |context| + next if ENV['SMITHY_RUBY_RBS_TEST'] && context != 'generated client gem' - expect(generated_shape.id).to eq(name) - expect(generated_shape).to be_a(shapes_module::OperationShape) - expect(generated_shape.input.id).to eq(shape['input']) - expect(generated_shape.output.id).to eq(shape['output']) - expect(generated_shape.traits).to eq(shape['traits']) - expect(generated_shape.errors.map(&:id)).to eq(shape['errors']) - end - end + context context do + include_examples 'shapes module', context end end end diff --git a/gems/smithy/spec/interfaces/client/types_spec.rb b/gems/smithy/spec/interfaces/client/types_spec.rb index eda94f704..6617297bd 100644 --- a/gems/smithy/spec/interfaces/client/types_spec.rb +++ b/gems/smithy/spec/interfaces/client/types_spec.rb @@ -1,43 +1,11 @@ # frozen_string_literal: true -describe 'Component: Types', rbs_test: true do - %i[schema client].each do |plan_type| - context "#{plan_type} generator" do - before(:all) do - @tmpdir = SpecHelper.generate(['ShapeService'], plan_type, fixture: 'shapes') - end +describe 'Client: Types', rbs_test: true do + ['generated client gem', 'generated client from source code'].each do |context| + next if ENV['SMITHY_RUBY_RBS_TEST'] && context != 'generated client gem' - after(:all) do - SpecHelper.cleanup(['ShapeService'], @tmpdir) - end - - it 'generates a types module' do - expect(ShapeService::Types).to be_a(Module) - end - - it 'has structures as structs that include Structure' do - expect(ShapeService::Types::Structure).to be < Struct - expect(ShapeService::Types::Structure).to include(Smithy::Client::Structure) - end - - it 'has unions that define member subclasses' do - expect(ShapeService::Types::Union).to be < Smithy::Client::Union - expect(ShapeService::Types::Union::Structure).to be < ShapeService::Types::Union - end - - it 'supports nested to_h' do - structure = ShapeService::Types::Structure.new(member: 'member') - union = ShapeService::Types::Union::Structure.new(structure) - input_output = ShapeService::Types::OperationInputOutput.new( - string: 'string', - union: union - ) - expected = { - string: 'string', - union: { structure: { member: 'member' } } - } - expect(input_output.to_h).to eq(expected) - end + context context do + include_examples 'types module', context end end end diff --git a/gems/smithy/spec/interfaces/schema/customizations_spec.rb b/gems/smithy/spec/interfaces/schema/customizations_spec.rb new file mode 100644 index 000000000..7cddec155 --- /dev/null +++ b/gems/smithy/spec/interfaces/schema/customizations_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +describe 'Schema: Customizations' do + include_examples 'customizations', 'generated schema gem' +end diff --git a/gems/smithy/spec/interfaces/schema/gemspec_spec.rb b/gems/smithy/spec/interfaces/schema/gemspec_spec.rb new file mode 100644 index 000000000..11e50750f --- /dev/null +++ b/gems/smithy/spec/interfaces/schema/gemspec_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +describe 'Schema: Gemspec' do + include_examples 'gemspec', 'generated schema gem' +end diff --git a/gems/smithy/spec/interfaces/schema/module_spec.rb b/gems/smithy/spec/interfaces/schema/module_spec.rb new file mode 100644 index 000000000..1e6f03225 --- /dev/null +++ b/gems/smithy/spec/interfaces/schema/module_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +describe 'Schema: Module' do + context 'single module' do + ['generated schema gem', 'generated schema from source code'].each do |context| + context context do + include_examples 'gem module', context + end + end + end +end diff --git a/gems/smithy/spec/interfaces/schema/shapes_spec.rb b/gems/smithy/spec/interfaces/schema/shapes_spec.rb new file mode 100644 index 000000000..1a7d78fc3 --- /dev/null +++ b/gems/smithy/spec/interfaces/schema/shapes_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +describe 'Schema: Shapes', rbs_test: true do + ['generated schema gem', 'generated schema from source code'].each do |context| + next if ENV['SMITHY_RUBY_RBS_TEST'] && context != 'generated schema gem' + + context context do + include_examples 'shapes module', context + end + end +end diff --git a/gems/smithy/spec/interfaces/schema/types_spec.rb b/gems/smithy/spec/interfaces/schema/types_spec.rb new file mode 100644 index 000000000..37c26299b --- /dev/null +++ b/gems/smithy/spec/interfaces/schema/types_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +describe 'Schema: Types', rbs_test: true do + ['generated schema gem', 'generated schema from source code'].each do |context| + next if ENV['SMITHY_RUBY_RBS_TEST'] && context != 'generated schema gem' + + context context do + include_examples 'types module', context + end + end +end diff --git a/gems/smithy/spec/interfaces/weld_spec.rb b/gems/smithy/spec/interfaces/weld_spec.rb index b7b03e42e..8ee18ea8b 100644 --- a/gems/smithy/spec/interfaces/weld_spec.rb +++ b/gems/smithy/spec/interfaces/weld_spec.rb @@ -37,34 +37,57 @@ def post_process(artifacts) end end end - - @tmpdir = SpecHelper.generate(['Weather'], :schema) end # rubocop:enable Lint/UselessAssignment - after(:all) do - SpecHelper.cleanup(['Weather'], @tmpdir) - end - it 'includes Thor::Actions' do expect(Class.new(Smithy::Weld).ancestors).to include(Thor::Actions) end - it 'can pre process the model' do - weld = Weather::Types::Weld.new - expect(weld).to be_a(Struct) - expect(weld.members).to be_empty - get_forecast_output = Weather::Types::GetForecastOutput.new - expect(get_forecast_output.members).to include(:chance_of_welds) + context 'generated client gem' do + include_context 'generated client gem', { fixture: 'weather' } + + it 'can pre process the model' do + weld = Weather::Types::Weld.new + expect(weld).to be_a(Struct) + expect(weld.members).to be_empty + get_forecast_output = Weather::Types::GetForecastOutput.new + expect(get_forecast_output.members).to include(:chance_of_welds) + end + + it 'can post process files' do + other_weld = Weather::Types::OtherWeld.new + expect(other_weld).to be_a(Struct) + end + + it 'does not apply welds that return false in #for?' do + expect(defined?(Weather::Types::WeldShouldNotExist)).to be nil + expect(defined?(Weather::Types::OtherWeldShouldNotExist)).to be nil + end end - it 'can post process files' do - other_weld = Weather::Types::OtherWeld.new - expect(other_weld).to be_a(Struct) + context 'generated schema gem' do + it 'pending' end - it 'does not apply welds that return false in #for?' do - expect(defined?(Weather::Types::WeldShouldNotExist)).to be nil - expect(defined?(Weather::Types::OtherWeldShouldNotExist)).to be nil + context 'source code' do + include_context 'generated client from source code', { fixture: 'weather' } + + it 'can pre process the model' do + weld = Weather::Types::Weld.new + expect(weld).to be_a(Struct) + expect(weld.members).to be_empty + get_forecast_output = Weather::Types::GetForecastOutput.new + expect(get_forecast_output.members).to include(:chance_of_welds) + end + + it 'cannot post process files' do + expect(defined?(Weather::Types::OtherWeld)).to be nil + end + + it 'does not apply welds that return false in #for?' do + expect(defined?(Weather::Types::WeldShouldNotExist)).to be nil + expect(defined?(Weather::Types::OtherWeldShouldNotExist)).to be nil + end end end diff --git a/gems/smithy/spec/spec_helper.rb b/gems/smithy/spec/spec_helper.rb index 2f112a564..290896b47 100644 --- a/gems/smithy/spec/spec_helper.rb +++ b/gems/smithy/spec/spec_helper.rb @@ -1,77 +1,55 @@ # frozen_string_literal: true -require 'json' require 'rspec' -require 'tmpdir' require 'stringio' require 'smithy' -require_relative 'support/be_in_documentation_matcher' +require_relative 'support/client_helper' require_relative 'support/rbs_spy_test' +require_relative 'support/contexts/generated_client_context' +require_relative 'support/contexts/generated_schema_context' + +require_relative 'support/examples/customizations_examples' +require_relative 'support/examples/gemspec_examples' +require_relative 'support/examples/module_examples' +require_relative 'support/examples/shapes_examples' +require_relative 'support/examples/types_examples' + +require_relative 'support/matchers/be_in_documentation_matcher' + module SpecHelper class << self - # @param [Array] modules A list of modules for the generated code. - # For example, `['Company', 'Weather']` would generate code in the - # `Company::Weather` namespace. - # @param [Symbol] type The type of service to generate. For example, - # `:schema`, `:client`, or `:server`. - # @param [Hash] options Additional options to pass to the generator. - # @option options [String] :fixture The name of the fixture to load. - # @return [String] The path to the directory where the generated code was - # written to. - def generate(modules, type, options = {}) - model = load_model(modules, options) - plan = create_plan(modules, model, type, options) - sdk_dir = plan.destination_root - Smithy.generate(plan) - - $LOAD_PATH << ("#{sdk_dir}/lib") + # (See ClientHelper#generate) + def generate_gem(type, options = {}) + plan = ClientHelper.generate(type, options) + $LOAD_PATH << "#{plan.destination_root}/lib" require plan.gem_name - - RbsSpyTest.setup(modules, sdk_dir) if ENV.fetch('SMITHY_RUBY_RBS_TEST', false) - sdk_dir - rescue StandardError => e - cleanup(modules, sdk_dir) if sdk_dir + RbsSpyTest.setup(plan.module_name, plan.destination_root) if ENV['SMITHY_RUBY_RBS_TEST'] + plan + rescue LoadError => e + puts "Error loading gem: #{plan.gem_name}" raise e end - # @param [Array] module_names A list of module names from the - # generated code to clean up. - # @param [String] tmpdir The path to the tmp directory where the - # generated code was written to. - def cleanup(module_names, tmpdir) - return unless tmpdir - - if ENV['SMITHY_RUBY_KEEP_GENERATED_SOURCE'] - puts "Leaving generated service in: #{tmpdir}" - else - FileUtils.rm_rf(tmpdir) - end - $LOAD_PATH.delete("#{tmpdir}/lib") - module_names.reverse.each_cons(2) do |child, parent| - Object.const_get(parent).send(:remove_const, child) - end - Object.send(:remove_const, module_names.first) + # (See ClientHelper#source) + def generate_from_source_code(type, options = {}) + module_name, source = ClientHelper.source(type, options) + Object.module_eval(source) + Object.const_get(module_name) + module_name + rescue LoadError => e + puts "Error evaluating source:\n#{source}" + raise e end - private - - def load_model(modules, options) - fixture = options[:fixture] || modules.map(&:underscore).join('/') - model_dir = File.join(File.dirname(__FILE__), 'fixtures', fixture) - JSON.load_file(File.join(model_dir, 'model.json')) + def cleanup_gem(plan) + ClientHelper.cleanup_gem(plan.module_name, plan.destination_root) end - def create_plan(modules, model, type, options) - plan_options = { - module_name: modules.join('::'), - gem_version: options.fetch(:gem_version, '0.1.0'), - destination_root: options.fetch(:destination_root, Dir.mktmpdir), - quiet: ENV.fetch('SMITHY_RUBY_QUIET', 'true') == 'true' - } - Smithy::Plan.new(model, type, plan_options) + def cleanup_modules(module_name) + ClientHelper.undefine_module(module_name) end end end diff --git a/gems/smithy/spec/support/client_helper.rb b/gems/smithy/spec/support/client_helper.rb new file mode 100644 index 000000000..ae108e98d --- /dev/null +++ b/gems/smithy/spec/support/client_helper.rb @@ -0,0 +1,182 @@ +# frozen_string_literal: true + +require 'tmpdir' + +module ClientHelper + class << self + def sample_shapes + { + 'smithy.ruby.tests#SampleClient' => { + 'type' => 'service', + 'operations' => [ + { 'target' => 'smithy.ruby.tests#Operation' } + ] + }, + 'smithy.ruby.tests#Operation' => { + 'type' => 'operation', + 'input' => { 'target' => 'smithy.ruby.tests#Structure' }, + 'output' => { 'target' => 'smithy.ruby.tests#Structure' } + }, + 'smithy.ruby.tests#Enum' => { + 'type' => 'enum', + 'members' => { + 'member' => { + 'target' => 'smithy.api#Unit', + 'traits' => { 'smithy.api#enumValue' => 'value' } + } + } + }, + 'smithy.ruby.tests#intEnum' => { + 'type' => 'intEnum', + 'members' => { + 'member' => { + 'target' => 'smithy.api#Unit', + 'traits' => { 'smithy.api#enumValue' => 1 } + } + } + }, + 'smithy.ruby.tests#List' => { + 'type' => 'list', + 'member' => { 'target' => 'smithy.api#String' } + }, + 'smithy.ruby.tests#Map' => { + 'type' => 'map', + 'key' => { 'target' => 'smithy.api#String' }, + 'value' => { 'target' => 'smithy.api#String' } + }, + 'smithy.ruby.tests#Structure' => { + 'type' => 'structure', + 'members' => { + 'bigDecimal' => { 'target' => 'smithy.api#BigDecimal' }, + 'bigInteger' => { 'target' => 'smithy.api#BigInteger' }, + 'blob' => { 'target' => 'smithy.api#Blob' }, + 'boolean' => { 'target' => 'smithy.api#Boolean' }, + 'byte' => { 'target' => 'smithy.api#Byte' }, + 'document' => { 'target' => 'smithy.api#Document' }, + 'double' => { 'target' => 'smithy.api#Double' }, + 'enum' => { 'target' => 'smithy.ruby.tests#Enum' }, + 'float' => { 'target' => 'smithy.api#Float' }, + 'intEnum' => { 'target' => 'smithy.ruby.tests#intEnum' }, + 'integer' => { 'target' => 'smithy.api#Integer' }, + 'list' => { 'target' => 'smithy.ruby.tests#List' }, + 'long' => { 'target' => 'smithy.api#Long' }, + 'map' => { 'target' => 'smithy.ruby.tests#Map' }, + 'short' => { 'target' => 'smithy.api#Short' }, + 'string' => { 'target' => 'smithy.api#String' }, + 'structure' => { 'target' => 'smithy.ruby.tests#Structure' }, + 'timestamp' => { 'target' => 'smithy.api#Timestamp' }, + 'union' => { 'target' => 'smithy.ruby.tests#Union' } + } + }, + 'smithy.ruby.tests#Union' => { + 'type' => 'union', + 'members' => { + 'string' => { 'target' => 'smithy.api#String' } + } + } + } + end + + # Generates a code artifact for the given type. + # @param [Symbol] type The type of artifact to generate. For example, + # `:schema` or `:client`. + # @param [Hash] options Additional options to pass to the generator. + # (See Plan#initialize) + # @option options [String] :fixture The name of the fixture to load in the + # fixtures folder relative to this file. Defaults to a sample model. + # @option options [Hash] :model The model to generate code for. Defaults to + # a sample model. + # @option options [String] :smithy The smithy version. Defaults to '2.0'. + # @option options [Hash] :shapes The shapes to generate code for. Defaults to + # a sample set of shapes. + def generate(type, options = {}) + model = load_model(options) + options[:destination_root] ||= Dir.mktmpdir + plan = create_plan(model, type, options) + Smithy.generate(plan) + plan + end + + # Generates source code for the given type. + # @param [Symbol] type The type of artifact to generate. For example, + # `:schema` or `:client`. + # @param [Hash] options Additional options to pass to the generator. + # (See Plan#initialize) + # @option options [String] :fixture The name of the fixture to load in the + # fixtures folder relative to this file. Defaults to a sample model. + # @option options [Hash] :model The model to generate code for. Defaults to + # a sample model. + # @option options [String] :smithy The smithy version. Defaults to '2.0'. + # @option options [Hash] :shapes The shapes to generate code for. Defaults to + # a sample set of shapes. + def source(type, options = {}) + model = load_model(options) + # options[:module_name] ||= next_sample_module_name + plan = create_plan(model, type, options) + source = Smithy.source(plan) + [plan.module_name, source] + end + + def cleanup_gem(module_name, tmpdir = nil) + undefine_module(module_name) + return unless tmpdir + + if ENV.fetch('SMITHY_RUBY_KEEP_GENERATED_SOURCE', 'false') == 'true' + puts "Leaving generated service in: #{tmpdir}" + else + FileUtils.rm_rf(tmpdir) + end + $LOAD_PATH.delete("#{tmpdir}/lib") + end + + def undefine_module(module_name) + module_names = module_name.split('::') + module_names.reverse.each_cons(2) do |child, parent| + Object.const_get(parent).send(:remove_const, child) + end + Object.send(:remove_const, module_names.first) + end + + private + + def create_plan(model, type, options = {}) + plan_options = { + gem_version: '0.1.0', + quiet: ENV.fetch('SMITHY_RUBY_QUIET', 'true') == 'true' + }.merge(options) + Smithy::Plan.new(model, type, plan_options) + end + + def load_model(options) + return load_fixture(options[:fixture]) if options[:fixture] + + options.fetch(:model, model(options)) + end + + def load_fixture(fixture) + model_dir = File.join(File.dirname(__FILE__), '..', 'fixtures', fixture) + JSON.load_file(File.join(model_dir, 'model.json')) + end + + def model(options) + { + 'smithy' => smithy(options), + 'shapes' => shapes(options) + } + end + + def smithy(options) + options[:smithy] || '2.0' + end + + def shapes(options) + options[:shapes] || sample_shapes + end + + # def next_sample_module_name + # @sample_client_count ||= 0 + # @sample_client_count += 1 + # "Sample#{@sample_client_count}" + # end + end +end diff --git a/gems/smithy/spec/support/contexts/generated_client_context.rb b/gems/smithy/spec/support/contexts/generated_client_context.rb new file mode 100644 index 000000000..75cb9c319 --- /dev/null +++ b/gems/smithy/spec/support/contexts/generated_client_context.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +RSpec.shared_context 'generated client gem' do |options| + before(:all) do + @plan = SpecHelper.generate_gem(:client, options) + end + + after(:all) do + SpecHelper.cleanup_gem(@plan) + end +end + +RSpec.shared_context 'generated client from source code' do |options| + before(:all) do + @module_name = SpecHelper.generate_from_source_code(:client, options) + end + + after(:all) do + SpecHelper.cleanup_modules(@module_name) + end +end diff --git a/gems/smithy/spec/support/contexts/generated_schema_context.rb b/gems/smithy/spec/support/contexts/generated_schema_context.rb new file mode 100644 index 000000000..8cf6d6297 --- /dev/null +++ b/gems/smithy/spec/support/contexts/generated_schema_context.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +RSpec.shared_context 'generated schema gem' do |options| + before(:all) do + @plan = SpecHelper.generate_gem(:schema, options) + end + + after(:all) do + SpecHelper.cleanup_gem(@plan) + end +end + +RSpec.shared_context 'generated schema from source code' do |options| + before(:all) do + @module_name = SpecHelper.generate_from_source_code(:schema, options) + end + + after(:all) do + SpecHelper.cleanup_modules(@module_name) + end +end diff --git a/gems/smithy/spec/support/examples/customizations_examples.rb b/gems/smithy/spec/support/examples/customizations_examples.rb new file mode 100644 index 000000000..91b8fa72e --- /dev/null +++ b/gems/smithy/spec/support/examples/customizations_examples.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'customizations' do |context| + include_context context, fixture: 'weather' + + let(:gem_name) do + context.include?('schema') ? 'weather-schema' : 'weather' + end + + it 'should have a customizations file' do + expect(File).to exist(File.join(@plan.destination_root, 'lib', gem_name, 'customizations.rb')) + end + + it 'should require the customizations file' do + expect(require("#{gem_name}/customizations")).to eq(false) + end + + it 'does not overwrite an existing customizations file' do + customization = <<~RUBY + module Weather + # @api private + module Customizations; end + end + RUBY + customizations_file = File.join(@plan.destination_root, 'lib', gem_name, 'customizations.rb') + expect(File.read(customizations_file)).to_not include(customization) + File.write(customizations_file, customization) + SpecHelper.generate_gem(@plan.type, fixture: 'weather', destination_root: @plan.destination_root) + expect(File.read(customizations_file)).to include(customization) + end +end diff --git a/gems/smithy/spec/support/examples/gemspec_examples.rb b/gems/smithy/spec/support/examples/gemspec_examples.rb new file mode 100644 index 000000000..ff6b5d3ac --- /dev/null +++ b/gems/smithy/spec/support/examples/gemspec_examples.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'gemspec' do |context| + context 'single module' do + include_context context, fixture: 'weather' + + let(:gem_name) do + if context.include?('schema') + 'weather-schema' + else + 'weather' + end + end + + it 'generates a gemspec with schema suffix' do + gemspec = File.join(@plan.destination_root, "#{gem_name}.gemspec") + expect(File.exist?(gemspec)).to be(true) + end + + it 'has a gem specification' do + gemspec = File.join(@plan.destination_root, "#{gem_name}.gemspec") + gem = Gem::Specification.load(gemspec) + expect(gem.name).to eq(gem_name) + expect(gem.version).to eq(Gem::Version.new('0.1.0')) + expect(gem.summary).to eq('Generated gem using Smithy') + expect(gem.authors).to eq(['Smithy Ruby']) + expect(gem.files).to include("lib/#{gem_name}/types.rb") + expect(gem.files).to include("lib/#{gem_name}/shapes.rb") + expect(gem.dependencies).to include(Gem::Dependency.new('smithy-client', '~> 1')) + end + end + + context 'nested module' do + include_context context, fixture: 'weather', module_name: 'SomeOrganization::Weather' + + let(:gem_name) do + if context.include?('schema') + 'some_organization-weather-schema' + else + 'some_organization-weather' + end + end + + it 'generates a gemspec with schema suffix' do + gemspec = File.join(@plan.destination_root, "#{gem_name}.gemspec") + expect(File.exist?(gemspec)).to be(true) + end + + it 'has a gem specification' do + gemspec = File.join(@plan.destination_root, "#{gem_name}.gemspec") + gem = Gem::Specification.load(gemspec) + expect(gem.name).to eq(gem_name) + expect(gem.version).to eq(Gem::Version.new('0.1.0')) + expect(gem.summary).to eq('Generated gem using Smithy') + expect(gem.authors).to eq(['Smithy Ruby']) + expect(gem.files).to include("lib/#{gem_name}/types.rb") + expect(gem.files).to include("lib/#{gem_name}/shapes.rb") + expect(gem.dependencies).to include(Gem::Dependency.new('smithy-client', '~> 1')) + end + end +end diff --git a/gems/smithy/spec/support/examples/module_examples.rb b/gems/smithy/spec/support/examples/module_examples.rb new file mode 100644 index 000000000..c4cc38a1a --- /dev/null +++ b/gems/smithy/spec/support/examples/module_examples.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'gem module' do |context| + include_context context, fixture: 'weather' + + context 'single module' do + it 'has a version' do + expect(Weather::VERSION).to eq('0.1.0') + end + + it 'requires interfaces' do + expect(Weather::Types).to be_a(Module) + expect(Weather::Shapes).to be_a(Module) + end + end + + context 'nested module' do + include_context context, fixture: 'weather', module_name: 'SomeOrganization::Weather' + + it 'has a version' do + expect(SomeOrganization::Weather::VERSION).to eq('0.1.0') + end + + it 'requires interfaces' do + expect(SomeOrganization::Weather::Types).to be_a(Module) + expect(SomeOrganization::Weather::Shapes).to be_a(Module) + end + end +end diff --git a/gems/smithy/spec/support/examples/shapes_examples.rb b/gems/smithy/spec/support/examples/shapes_examples.rb new file mode 100644 index 000000000..417a3ebee --- /dev/null +++ b/gems/smithy/spec/support/examples/shapes_examples.rb @@ -0,0 +1,399 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'shapes module' do |context| + include_context context, fixture: 'shapes' + + let(:fixture) { JSON.load_file(File.expand_path('../../fixtures/shapes/model.json', __dir__.to_s)) } + + subject { ShapeService::Shapes } + + it 'generates a shapes module' do + expect(ShapeService::Shapes).to be_a(Module) + end + + def expect_generated_shape(subject, shape_class, shape_hash) + id, shape = shape_hash + expect(subject).to be_a(shape_class) + expect(subject.id).to eq(id) + expect(subject.traits).to eq(shape['traits']) + end + + context 'blob' do + subject { ShapeService::Shapes::Blob } + let(:shape_class) { Smithy::Client::Shapes::BlobShape } + let(:shape_hash) do + fixture['shapes'].find { |_, s| s['type'] == 'blob' } + end + + it 'generates a blob shape' do + expect_generated_shape(subject, shape_class, shape_hash) + end + end + + context 'boolean' do + subject { ShapeService::Shapes::Boolean } + let(:shape_class) { Smithy::Client::Shapes::BooleanShape } + let(:shape_hash) do + fixture['shapes'].find { |_, s| s['type'] == 'boolean' } + end + + it 'generates a boolean shape' do + expect_generated_shape(subject, shape_class, shape_hash) + end + end + + context 'string' do + subject { ShapeService::Shapes::String } + let(:shape_class) { Smithy::Client::Shapes::StringShape } + let(:shape_hash) do + fixture['shapes'].find { |_, s| s['type'] == 'string' } + end + + it 'generates a string shape' do + expect_generated_shape(subject, shape_class, shape_hash) + end + end + + context 'byte' do + subject { ShapeService::Shapes::Byte } + let(:shape_class) { Smithy::Client::Shapes::IntegerShape } + let(:shape_hash) do + fixture['shapes'].find { |_, s| s['type'] == 'byte' } + end + + it 'generates a byte shape' do + expect_generated_shape(subject, shape_class, shape_hash) + end + end + + context 'short' do + subject { ShapeService::Shapes::Short } + let(:shape_class) { Smithy::Client::Shapes::IntegerShape } + let(:shape_hash) do + fixture['shapes'].find { |_, s| s['type'] == 'short' } + end + + it 'generates a short shape' do + expect_generated_shape(subject, shape_class, shape_hash) + end + end + + context 'integer' do + subject { ShapeService::Shapes::Integer } + let(:shape_class) { Smithy::Client::Shapes::IntegerShape } + let(:shape_hash) do + fixture['shapes'].find { |_, s| s['type'] == 'integer' } + end + + it 'generates an integer shape' do + expect_generated_shape(subject, shape_class, shape_hash) + end + end + + context 'long' do + subject { ShapeService::Shapes::Long } + let(:shape_class) { Smithy::Client::Shapes::IntegerShape } + let(:shape_hash) do + fixture['shapes'].find { |_, s| s['type'] == 'long' } + end + + it 'generates a long shape' do + expect_generated_shape(subject, shape_class, shape_hash) + end + end + + context 'float' do + subject { ShapeService::Shapes::Float } + let(:shape_class) { Smithy::Client::Shapes::FloatShape } + let(:shape_hash) do + fixture['shapes'].find { |_, s| s['type'] == 'float' } + end + + it 'generates a float shape' do + expect_generated_shape(subject, shape_class, shape_hash) + end + end + + context 'double' do + subject { ShapeService::Shapes::Double } + let(:shape_class) { Smithy::Client::Shapes::FloatShape } + let(:shape_hash) do + fixture['shapes'].find { |_, s| s['type'] == 'double' } + end + + it 'generates a double shape' do + expect_generated_shape(subject, shape_class, shape_hash) + end + end + + context 'bigInteger' do + subject { ShapeService::Shapes::BigInteger } + let(:shape_class) { Smithy::Client::Shapes::IntegerShape } + let(:shape_hash) do + fixture['shapes'].find { |_, s| s['type'] == 'bigInteger' } + end + + it 'generates a big integer shape' do + expect_generated_shape(subject, shape_class, shape_hash) + end + end + + context 'bigDecimal' do + subject { ShapeService::Shapes::BigDecimal } + let(:shape_class) { Smithy::Client::Shapes::BigDecimalShape } + let(:shape_hash) do + fixture['shapes'].find { |_, s| s['type'] == 'bigDecimal' } + end + + it 'generates a big decimal shape' do + expect_generated_shape(subject, shape_class, shape_hash) + end + end + + context 'timestamp' do + subject { ShapeService::Shapes::Timestamp } + let(:shape_class) { Smithy::Client::Shapes::TimestampShape } + let(:shape_hash) do + fixture['shapes'].find { |_, s| s['type'] == 'timestamp' } + end + + it 'generates a timestamp shape' do + expect_generated_shape(subject, shape_class, shape_hash) + end + end + + context 'document' do + subject { ShapeService::Shapes::Document } + let(:shape_class) { Smithy::Client::Shapes::DocumentShape } + let(:shape_hash) do + fixture['shapes'].find { |_, s| s['type'] == 'document' } + end + + it 'generates a document shape' do + expect_generated_shape(subject, shape_class, shape_hash) + end + end + + context 'enum' do + subject { ShapeService::Shapes::Enum } + let(:shape_class) { Smithy::Client::Shapes::EnumShape } + let(:shape_hash) do + fixture['shapes'].find { |_, s| s['type'] == 'enum' } + end + let(:expected_member) do + _, shape = shape_hash + shape['members']['FOO'] + end + + it 'generates an enum shape' do + expect_generated_shape(subject, shape_class, shape_hash) + end + + it 'has members' do + expect(subject.members.keys).to eq(%i[foo]) + expect(subject.members[:foo]).to be_a(Smithy::Client::Shapes::MemberShape) + expect(subject.members[:foo].shape).to be_a(Smithy::Client::Shapes::StructureShape) + expect(subject.members[:foo].traits).to eq(expected_member['traits']) + expect(subject.members[:foo].shape.id).to eq(expected_member['target']) + end + + it 'has a member with traits' do + expect(subject.member(:foo).traits).to eq(expected_member['traits']) + end + end + + context 'intEnum' do + subject { ShapeService::Shapes::IntEnum } + let(:shape_class) { Smithy::Client::Shapes::IntEnumShape } + let(:shape_hash) do + fixture['shapes'].find { |_, s| s['type'] == 'intEnum' } + end + let(:expected_member) do + _, shape = shape_hash + shape['members']['BAZ'] + end + + it 'generates an int enum shape' do + expect_generated_shape(subject, shape_class, shape_hash) + end + + it 'has members' do + expect(subject.members.keys).to eq(%i[baz]) + expect(subject.members[:baz]).to be_a(Smithy::Client::Shapes::MemberShape) + expect(subject.members[:baz].shape).to be_a(Smithy::Client::Shapes::StructureShape) + expect(subject.members[:baz].traits).to eq(expected_member['traits']) + expect(subject.members[:baz].shape.id).to eq(expected_member['target']) + end + + it 'has a member with traits' do + expect(subject.member(:baz).traits).to eq(expected_member['traits']) + end + end + + context 'list' do + subject { ShapeService::Shapes::List } + let(:shape_class) { Smithy::Client::Shapes::ListShape } + let(:shape_hash) do + fixture['shapes'].find { |_, s| s['type'] == 'list' } + end + let(:expected_member) do + _, shape = shape_hash + shape['member'] + end + + it 'generates a list shape' do + expect_generated_shape(subject, shape_class, shape_hash) + end + + it 'has a member' do + expect(subject.member).to be_a(Smithy::Client::Shapes::MemberShape) + expect(subject.member.shape).to be_a(Smithy::Client::Shapes::StringShape) + expect(subject.member.shape.id).to eq(expected_member['target']) + end + + it 'has a member with traits' do + expect(subject.member.traits).to eq(expected_member['traits']) + end + end + + context 'map' do + subject { ShapeService::Shapes::Map } + let(:shape_class) { Smithy::Client::Shapes::MapShape } + let(:shape_hash) do + fixture['shapes'].find { |_, s| s['type'] == 'map' } + end + let(:expected_shape) do + _, shape = shape_hash + shape + end + + it 'generates a map shape' do + expect_generated_shape(subject, shape_class, shape_hash) + end + + it 'has key and value members' do + expect(subject.key).to be_a(Smithy::Client::Shapes::MemberShape) + expect(subject.key.shape).to be_a(Smithy::Client::Shapes::StringShape) + expect(subject.key.shape.id).to eq(expected_shape['key']['target']) + expect(subject.value).to be_a(Smithy::Client::Shapes::MemberShape) + expect(subject.value.shape).to be_a(Smithy::Client::Shapes::StringShape) + expect(subject.value.shape.id).to eq(expected_shape['value']['target']) + end + + it 'has keys and values with traits' do + expect(subject.key.traits).to eq(expected_shape['key']['traits']) + expect(subject.value.traits).to eq(expected_shape['value']['traits']) + end + end + + context 'union' do + subject { ShapeService::Shapes::Union } + let(:shape_class) { Smithy::Client::Shapes::UnionShape } + let(:shape_hash) do + fixture['shapes'].find { |_, s| s['type'] == 'union' } + end + let(:expected_shape) do + _, shape = shape_hash + shape + end + + it 'generates a union shape' do + expect_generated_shape(subject, shape_class, shape_hash) + end + + it 'has members' do + expected_members = expected_shape['members'].keys.map(&:to_sym) + expect(subject.members.keys).to eq(expected_members) + end + + it 'has a member with traits' do + expected_member = expected_shape['members'].slice('string').values.first + expect(subject.member(:string).traits).to eq(expected_member['traits']) + end + + it 'has a type' do + expect(subject.type).to eq(ShapeService::Types::Union) + end + + it 'has members with types' do + expect(subject.member(:string)).to be_a(Smithy::Client::Shapes::MemberShape) + expect(subject.member_type(:string)).to eq(ShapeService::Types::Union::String) + end + end + + context 'structure' do + subject { ShapeService::Shapes::Structure } + let(:shape_class) { Smithy::Client::Shapes::StructureShape } + let(:shape_hash) do + fixture['shapes'].find { |k, _| k.include?('Structure') } + end + let(:expected_shape) do + _, shape = shape_hash + shape + end + + it 'generates a structure shape' do + expect_generated_shape(subject, shape_class, shape_hash) + end + + it 'has members' do + expected_members = + expected_shape['members'] + .keys + .map { |m| m.underscore.to_sym } + expect(subject.members.keys).to eq(expected_members) + end + + it 'has a member with traits' do + expected_member = + expected_shape['members'] + .slice('member') + .values + .first + expect(subject.member(:member).traits).to eq(expected_member['traits']) + end + end + + context 'schema' do + it 'is a schema' do + expect(subject::SCHEMA).to be_a(Smithy::Client::Schema) + end + + context 'service' do + let(:service_shape) { subject::SCHEMA.service } + let(:expected_service) { fixture['shapes'].find { |_k, v| v['type'] == 'service' } } + + it 'is a service shape and able to access service shape data' do + expect(service_shape).to be_a(Smithy::Client::Shapes::ServiceShape) + expect(service_shape.id).to eql(expected_service[0]) + expect(service_shape.version).to eq(expected_service[1]['version']) + + if (expected_traits = expected_service[1]['traits']) + expect(service_shape.traits).to include(expected_traits) + end + end + end + + context 'operations' do + let(:operations) { subject::SCHEMA.operations } + let(:operation_shapes) { fixture.select { |_k, v| v['type'] == 'operation' } } + + it 'is not empty' do + expect(operations).not_to be_empty + end + + it 'made of operation shapes and able to access its contents' do + operation_shapes.each do |name, shape| + generated_shape = subject::SCHEMA.operation(name.underscore) + + expect(generated_shape.id).to eq(name) + expect(generated_shape).to be_a(shapes_module::OperationShape) + expect(generated_shape.input.id).to eq(shape['input']) + expect(generated_shape.output.id).to eq(shape['output']) + expect(generated_shape.traits).to eq(shape['traits']) + expect(generated_shape.errors.map(&:id)).to eq(shape['errors']) + end + end + end + end +end diff --git a/gems/smithy/spec/support/examples/types_examples.rb b/gems/smithy/spec/support/examples/types_examples.rb new file mode 100644 index 000000000..8d3e65a6c --- /dev/null +++ b/gems/smithy/spec/support/examples/types_examples.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'types module' do |context| + include_context context, fixture: 'shapes' + + it 'generates a types module' do + expect(ShapeService::Types).to be_a(Module) + end + + it 'has structures as structs that include Structure' do + expect(ShapeService::Types::Structure).to be < Struct + expect(ShapeService::Types::Structure).to include(Smithy::Client::Structure) + end + + it 'has unions that define member subclasses' do + expect(ShapeService::Types::Union).to be < Smithy::Client::Union + expect(ShapeService::Types::Union::Structure).to be < ShapeService::Types::Union + end + + it 'supports nested to_h' do + structure = ShapeService::Types::Structure.new(member: 'member') + union = ShapeService::Types::Union::Structure.new(structure) + input_output = ShapeService::Types::OperationInputOutput.new( + string: 'string', + union: union + ) + expected = { + string: 'string', + union: { structure: { member: 'member' } } + } + expect(input_output.to_h).to eq(expected) + end +end diff --git a/gems/smithy/spec/support/be_in_documentation_matcher.rb b/gems/smithy/spec/support/matchers/be_in_documentation_matcher.rb similarity index 100% rename from gems/smithy/spec/support/be_in_documentation_matcher.rb rename to gems/smithy/spec/support/matchers/be_in_documentation_matcher.rb diff --git a/gems/smithy/spec/support/rbs_spy_test.rb b/gems/smithy/spec/support/rbs_spy_test.rb index ba0e1a574..08842213f 100644 --- a/gems/smithy/spec/support/rbs_spy_test.rb +++ b/gems/smithy/spec/support/rbs_spy_test.rb @@ -5,7 +5,7 @@ # Utility to set up RBS spy test on generated code. module RbsSpyTest class << self - def setup(modules, sdk_dir) + def setup(module_name, sdk_dir) env = load_rbs_environment(sdk_dir) tester = RBS::Test::Tester.new(env: env) @@ -16,10 +16,10 @@ def setup(modules, sdk_dir) ::RSpec::Mocks::ClassVerifyingDouble ] - spy_modules = [modules.join('::'), 'Smithy::Client'] + spy_modules = [module_name, 'Smithy::Client'] spy_classes = [] - spy_modules.each do |module_name| - spy_classes += classes_to_spy(Object.const_get(module_name), env) + spy_modules.each do |spy_module_name| + spy_classes += classes_to_spy(Object.const_get(spy_module_name), env) end spy_classes.each do |spy_class|