diff --git a/.rubocop.yml b/.rubocop.yml index e03f953fb..481a9def0 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -17,13 +17,13 @@ Metrics/BlockLength: Metrics/ClassLength: Max: 200 +Metrics/MethodLength: + Max: 20 + Metrics/ModuleLength: Exclude: - '**/spec/**/*.rb' -Metrics/MethodLength: - Max: 20 - Naming/FileName: Exclude: - '**/spec/**/*.rb' diff --git a/README.md b/README.md index a505e7271..e3b63ae55 100644 --- a/README.md +++ b/README.md @@ -17,15 +17,21 @@ local build using smithy cli bundle exec smithy build --debug model/weather.smithy ``` -local build using smithy-ruby executable +local build using smithy-ruby executable: ``` export SMITHY_PLUGIN_DIR=build/smithy/source/smithy-ruby bundle exec smithy-ruby smith client --gem-name weather --gem-version 1.0.0 --destination-root projections/weather <<< $(smithy ast model/weather.smithy) ``` -IRB on weather gem +IRB on `weather` gem: ``` -irb -I build/smithy/weather/smithy-ruby/lib -I gems/smithy-client/lib -r weather +irb -I projections/weather/lib -I gems/smithy-client/lib -r weather +``` + +Create a Weather client: +``` +client = Weather::Client.new(endpoint: 'https://example.com') +client.get_current_time ``` Build a fixture diff --git a/gems/smithy-client/lib/smithy-client.rb b/gems/smithy-client/lib/smithy-client.rb index 2ab0ecdaf..f8be3f0ae 100644 --- a/gems/smithy-client/lib/smithy-client.rb +++ b/gems/smithy-client/lib/smithy-client.rb @@ -36,10 +36,10 @@ # model -require_relative 'smithy-client/api' require_relative 'smithy-client/base' require_relative 'smithy-client/errors' -require_relative 'smithy-client/operation' +require_relative 'smithy-client/schema' +require_relative 'smithy-client/shapes' require_relative 'smithy-client/structure' # endpoints diff --git a/gems/smithy-client/lib/smithy-client/base.rb b/gems/smithy-client/lib/smithy-client/base.rb index 3e79f3f35..40e5c1959 100644 --- a/gems/smithy-client/lib/smithy-client/base.rb +++ b/gems/smithy-client/lib/smithy-client/base.rb @@ -44,7 +44,7 @@ def build_input(operation_name, params = {}) # names. These are valid arguments to {#build_input} and are also # valid methods. def operation_names - self.class.api.operation_names + self.class.schema.operation_names end # @api private @@ -58,12 +58,12 @@ def inspect # opportunity to register options with default values. def build_config(plugins, options) config = Configuration.new - config.add_option(:api) + config.add_option(:schema) config.add_option(:plugins) plugins.each do |plugin| plugin.add_options(config) if plugin.respond_to?(:add_options) end - config.build!(options.merge(api: self.class.api)) + config.build!(options.merge(schema: self.class.schema)) end # Gives each plugin the opportunity to register handlers for this client. @@ -84,7 +84,7 @@ def after_initialize(plugins) def context_for(operation_name, params) HandlerContext.new( operation_name: operation_name, - operation: config.api.operation(operation_name), + operation: config.schema.operation(operation_name), client: self, params: params, config: config @@ -162,24 +162,24 @@ def plugins Array(@plugins).freeze end - # @return [API] - def api - @api ||= API.new + # @return [Schema] + def schema + @schema ||= Schema.new end - # @param [API] api - def api=(api) - @api = api + # @param [Schema] schema + def schema=(schema) + @schema = schema define_operation_methods end - # @option options [API] :api (API.new) + # @option options [Schema] :schema (Schema.new) # @option options [Array] :plugins ([]) A list of plugins to # add to the client class created. # @return [Class] def define(options = {}) subclass = Class.new(self) - subclass.api = options[:api] || api + subclass.schema = options[:schema] || schema Array(options[:plugins]).each do |plugin| subclass.add_plugin(plugin) end @@ -191,7 +191,7 @@ def define(options = {}) def define_operation_methods operations_module = Module.new - @api.operation_names.each do |method_name| + @schema.operation_names.each do |method_name| operations_module.send(:define_method, method_name) do |*args, &block| params = args[0] || {} options = args[1] || {} diff --git a/gems/smithy-client/lib/smithy-client/handler_context.rb b/gems/smithy-client/lib/smithy-client/handler_context.rb index e41e59501..aab38dc9a 100644 --- a/gems/smithy-client/lib/smithy-client/handler_context.rb +++ b/gems/smithy-client/lib/smithy-client/handler_context.rb @@ -5,7 +5,7 @@ module Client # Context that is passed to handlers during execution. class HandlerContext # @option options [Symbol] :operation_name (nil) - # @option options [Operation] :operation (nil) + # @option options [OperationShape] :operation (nil) # @option options [Base] :client (nil) # @option options [Hash] :params ({}) # @option options [Configuration] :config (nil) @@ -26,7 +26,7 @@ def initialize(options = {}) # @return [Symbol] Name of the API operation called. attr_accessor :operation_name - # @return [Operation] + # @return [OperationShape] Shape of the Operation called. attr_accessor :operation # @return [Base] diff --git a/gems/smithy-client/lib/smithy-client/operation.rb b/gems/smithy-client/lib/smithy-client/operation.rb deleted file mode 100644 index e647d237b..000000000 --- a/gems/smithy-client/lib/smithy-client/operation.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -module Smithy - module Client - # @api private - class Operation - def initialize - @errors = [] - yield self if block_given? - end - - # @return [String, nil] - attr_accessor :name - - # @return [Shape, nil] - attr_accessor :input - - # @return [Shape, nil] - attr_accessor :output - - # @return [Array] - attr_accessor :errors - end - end -end diff --git a/gems/smithy-client/lib/smithy-client/api.rb b/gems/smithy-client/lib/smithy-client/schema.rb similarity index 68% rename from gems/smithy-client/lib/smithy-client/api.rb rename to gems/smithy-client/lib/smithy-client/schema.rb index 07058ddbd..934b06e8e 100644 --- a/gems/smithy-client/lib/smithy-client/api.rb +++ b/gems/smithy-client/lib/smithy-client/schema.rb @@ -3,32 +3,38 @@ module Smithy module Client # @api private - class API + class Schema include Enumerable def initialize - @metadata = {} + @service = nil @operations = {} yield self if block_given? end - # @return [String, nil] - attr_accessor :version + # @return [ServiceShape, nil] + attr_accessor :service - # @return [Hash] - attr_accessor :metadata + # @return [Hash] + attr_accessor :operations + # @return [OperationShape] + def add_operation(name, operation) + @operations[name] = operation + end + + # @return [Hash] def each(&) @operations.each(&) end - # @return [Array] - def add_operation(name, operation) - @operations[name] = operation + # @return [String] + def inspect + "#<#{self.class.name}>" end # @param [String] name - # @return [Operation] + # @return [OperationShape] operation def operation(name) raise ArgumentError, "unknown operation #{name.inspect}" unless @operations.key?(name) @@ -39,11 +45,6 @@ def operation(name) def operation_names @operations.keys end - - # @api private - def inspect - "#<#{self.class.name}>" - end end end end diff --git a/gems/smithy-client/lib/smithy-client/shapes.rb b/gems/smithy-client/lib/smithy-client/shapes.rb new file mode 100644 index 000000000..433fde898 --- /dev/null +++ b/gems/smithy-client/lib/smithy-client/shapes.rb @@ -0,0 +1,225 @@ +# frozen_string_literal: true + +module Smithy + module Client + # Represents shape types from Smithy Model + module Shapes + # A base shape that all shapes inherits from + class Shape + def initialize(options = {}) + @id = options[:id] + @traits = options[:traits] || {} + end + + # @return [String, nil] + attr_accessor :id + + # @return [Hash] + attr_accessor :traits + end + + # Represents a slim variation of the Service shape + class ServiceShape < Shape + def initialize(options = {}) + super + @version = options[:version] + end + + # @return [String, nil] + attr_accessor :version + end + + # Represents an Operation shape + class OperationShape < Shape + def initialize(options = {}) + super + @input = options[:input] + @output = options[:output] + @errors = options[:errors] || [] + yield self if block_given? + end + + # @return [StructureShape, nil] + attr_accessor :input + + # @return [StructureShape, nil] + attr_accessor :output + + # @return [Array] + attr_accessor :errors + end + + # Represents BigDecimal shape + class BigDecimalShape < Shape; end + + # Represents both Blob and Data Stream shapes + class BlobShape < Shape; end + + # Represents a Boolean shape + class BooleanShape < Shape; end + + # Represents a Document shape + class DocumentShape < Shape; end + + # Represents an Enum shape + class EnumShape < Shape + def initialize(options = {}) + super + @members = {} + end + + # @return [Hash] + attr_accessor :members + + # @return [MemberShape] + def add_member(name, shape, traits) + @members[name] = MemberShape.new(name, shape, traits) + end + end + + # Represents the following shapes: + # Byte, Short, Integer, Long, BigInteger + class IntegerShape < Shape; end + + # Represents an IntEnum shape + class IntEnumShape < EnumShape; end + + # Represents both Float and double shapes + class FloatShape < Shape; end + + # Represents a List shape + class ListShape < Shape + def initialize(options = {}) + super + @member = nil + end + + # @return [MemberShape, nil] + attr_accessor :member + + # @return [MemberShape] + def set_member(shape, traits) + @member = MemberShape.new('member', shape, traits) + end + end + + # Represents a Map shape + class MapShape < Shape + def initialize(options = {}) + super + @key = nil + @value = nil + end + + # @return [MemberShape, nil] + attr_accessor :key + + # @return [MemberShape, nil] + attr_accessor :value + + # @return [MemberShape] + def set_key(shape, traits) + @key = MemberShape.new('key', shape, traits) + end + + # @return [MemberShape] + def set_value(shape, traits) + @value = MemberShape.new('value', shape, traits) + end + end + + # Represents the String shape + class StringShape < Shape; end + + # Represents the Structure shape + class StructureShape < Shape + def initialize(options = {}) + super + @members = {} + @type = nil + end + + # @return [Hash] + attr_accessor :members + + # @return [Struct] + attr_accessor :type + + # @return [MemberShape] + def add_member(name, shape, traits) + @members[name] = MemberShape.new(name, shape, traits) + end + end + + # Represents the Timestamp shape + class TimestampShape < Shape; end + + # Represents both Union and Eventstream shapes + class UnionShape < StructureShape; end + + # Represents a member shape + class MemberShape + def initialize(name, shape, traits) + @name = name + @shape = shape + @traits = traits + end + + # @return [String] + attr_accessor :name + + # @return [Shape] + attr_accessor :shape + + # @return [Hash] + attr_accessor :traits + end + + BigDecimal = BigDecimalShape.new(id: 'smithy.api#BigDecimal') + BigInteger = IntegerShape.new(id: 'smithy.api#BigInteger') + Blob = BlobShape.new(id: 'smithy.api#Blob') + Boolean = BooleanShape.new(id: 'smithy.api#Boolean') + Byte = IntegerShape.new(id: 'smithy.api#Byte') + Document = DocumentShape.new(id: 'smithy.api#Document') + Double = FloatShape.new(id: 'smithy.api#Double') + Float = FloatShape.new(id: 'smithy.api#Float') + Integer = IntegerShape.new(id: 'smithy.api#Integer') + Long = IntegerShape.new(id: 'smithy.api#Long') + PrimitiveBoolean = BooleanShape.new( + id: 'smithy.api#PrimitiveBoolean', + traits: { 'smithy.api#default' => false } + ) + PrimitiveByte = IntegerShape.new( + id: 'smithy.api#PrimitiveByte', + traits: { 'smithy.api#default' => 0 } + ) + PrimitiveDouble = FloatShape.new( + id: 'smithy.api#PrimitiveDouble', + traits: { 'smithy.api#default' => 0 } + ) + PrimitiveFloat = FloatShape.new( + id: 'smithy.api#PrimitiveFloat', + traits: { 'smithy.api#default' => 0 } + ) + PrimitiveInteger = IntegerShape.new( + id: 'smithy.api#PrimitiveInteger', + traits: { 'smithy.api#default' => 0 } + ) + PrimitiveShort = IntegerShape.new( + id: 'smithy.api#PrimitiveShort', + traits: { 'smithy.api#default' => 0 } + ) + PrimitiveLong = IntegerShape.new( + id: 'smithy.api#PrimitiveLong', + traits: { 'smithy.api#default' => 0 } + ) + Short = IntegerShape.new(id: 'smithy.api#Short') + String = StringShape.new(id: 'smithy.api#String') + Timestamp = TimestampShape.new(id: 'smithy.api#Timestamp') + Unit = StructureShape.new( + id: 'smithy.api#Unit', + traits: { 'smithy.api#unitType' => {} } + ) + end + end +end diff --git a/gems/smithy-client/spec/smithy-client/api_spec.rb b/gems/smithy-client/spec/smithy-client/api_spec.rb deleted file mode 100644 index 2742e4605..000000000 --- a/gems/smithy-client/spec/smithy-client/api_spec.rb +++ /dev/null @@ -1,91 +0,0 @@ -# frozen_string_literal: true - -module Smithy - module Client - describe API do - subject { API.new } - - it 'is enumerable' do - expect(subject).to be_kind_of(Enumerable) - end - - describe '#initialize' do - it 'yields itself' do - yielded = nil - subject = API.new { |api| yielded = api } - expect(yielded).to be(subject) - end - end - - describe '#version' do - it 'defaults to nil' do - expect(subject.version).to be(nil) - end - - it 'can be set' do - subject.version = '2015-01-01' - expect(subject.version).to eq('2015-01-01') - end - end - - describe '#metadata' do - it 'defaults to {}' do - expect(subject.metadata).to eq({}) - end - - it 'can be populated' do - subject.metadata['key'] = 'value' - expect(subject.metadata['key']).to eq('value') - end - end - - describe '#each' do - it 'enumerates over operations' do - operation = Operation.new - subject.add_operation(:name, operation) - expect { |b| subject.each(&b) }.to yield_successive_args([:name, operation]) - end - end - - describe '#add_operation' do - it 'adds an operation' do - operation = Operation.new - subject.add_operation(:name, operation) - expect(subject.operation(:name)).to be(operation) - end - end - - describe '#operation' do - it 'raises an ArgumentError for unknown operations' do - expect do - subject.operation(:unknown) - end.to raise_error(ArgumentError, 'unknown operation :unknown') - end - - it 'returns the operation' do - operation = Operation.new - subject.add_operation(:name, operation) - expect(subject.operation(:name)).to be(operation) - end - end - - describe '#operation_names' do - it 'defaults to an empty array' do - expect(subject.operation_names).to eq([]) - end - - it 'provides operation names' do - subject.add_operation(:operation1, Operation.new) - subject.add_operation(:operation2, Operation.new) - expect(subject.operation_names).to eq(%i[operation1 operation2]) - end - end - - describe '#inspect' do - it 'returns the class name' do - expect(subject.inspect).to eq('#') - end - end - end - end -end diff --git a/gems/smithy-client/spec/smithy-client/base_spec.rb b/gems/smithy-client/spec/smithy-client/base_spec.rb index ebc52fb0b..ef85e5c71 100644 --- a/gems/smithy-client/spec/smithy-client/base_spec.rb +++ b/gems/smithy-client/spec/smithy-client/base_spec.rb @@ -3,8 +3,8 @@ module Smithy module Client describe Base do - let(:api) { API.new } - let(:client_class) { Base.define(api: api) } + let(:schema) { Schema.new } + let(:client_class) { Base.define(schema: schema) } let(:plugin_a) { Plugin.new } let(:plugin_b) { Plugin.new } @@ -19,8 +19,8 @@ module Client expect(subject.config).to be_kind_of(Struct) end - it 'contains the client api' do - expect(subject.config.api).to be(client_class.api) + it 'contains a schema' do + expect(subject.config.schema).to be(client_class.schema) end it 'contains instance plugins' do @@ -51,7 +51,7 @@ module Client let(:input) { subject.build_input(:operation_name) } before(:each) do - api.add_operation(:operation_name, Operation.new) + schema.add_operation(:operation_name, Shapes::OperationShape.new) end it 'returns an Input' do @@ -107,7 +107,7 @@ module Client let(:input) { Input.new } before(:each) do - api.add_operation(:operation_name, Operation.new) + schema.add_operation(:operation_name, Shapes::OperationShape.new) allow(subject).to receive(:build_input).and_return(input) allow(input).to receive(:send_request) end @@ -297,17 +297,17 @@ module Client end end - describe '.api' do - it 'defaults to an API' do - expect(client_class.api).to be_kind_of(API) + describe '.schema' do + it 'defaults to a Schema' do + expect(client_class.schema).to be_kind_of(Schema) end end - describe '.api=' do + describe '.schema=' do it 'can be set' do - api = API.new - client_class.api = api - expect(client_class.api).to be(api) + schema = Schema.new + client_class.schema = schema + expect(client_class.schema).to be(schema) end end @@ -317,10 +317,10 @@ module Client expect(client_class.ancestors).to include(Client::Base) end - it 'sets the api on the client class' do - api = API.new - client_class = Base.define(api: api) - expect(client_class.api).to be(api) + it 'sets the schema on the client class' do + schema = Schema.new + client_class = Base.define(schema: schema) + expect(client_class.schema).to be(schema) end it 'extends from subclasses of client' do diff --git a/gems/smithy-client/spec/smithy-client/handler_context_spec.rb b/gems/smithy-client/spec/smithy-client/handler_context_spec.rb index 6f8f2bd43..19f4c1fa1 100644 --- a/gems/smithy-client/spec/smithy-client/handler_context_spec.rb +++ b/gems/smithy-client/spec/smithy-client/handler_context_spec.rb @@ -22,7 +22,7 @@ module Client end it 'can be set in the constructor' do - operation = Operation.new + operation = Shapes::OperationShape.new context = HandlerContext.new(operation: operation) expect(context.operation).to be(operation) end diff --git a/gems/smithy-client/spec/smithy-client/operation_spec.rb b/gems/smithy-client/spec/smithy-client/operation_spec.rb deleted file mode 100644 index 9eb467623..000000000 --- a/gems/smithy-client/spec/smithy-client/operation_spec.rb +++ /dev/null @@ -1,70 +0,0 @@ -# frozen_string_literal: true - -module Smithy - module Client - describe Operation do - subject { Operation.new } - - describe '#initialize' do - it 'yields itself' do - yielded = nil - subject = Operation.new { |api| yielded = api } - expect(yielded).to be(subject) - end - end - - describe '#name' do - it 'defaults to nil' do - expect(subject.name).to be(nil) - end - - it 'can be set' do - subject.name = 'OperationName' - expect(subject.name).to eq('OperationName') - end - end - - describe '#input' do - it 'defaults to nil' do - expect(subject.input).to be(nil) - end - - it 'can be set' do - shape = double('shape') - subject.input = shape - expect(subject.input).to be(shape) - end - end - - describe '#output' do - it 'defaults to nil' do - expect(subject.output).to be(nil) - end - - it 'can be set' do - shape = double('shape') - subject.output = shape - expect(subject.output).to be(shape) - end - end - - describe '#errors' do - it 'defaults to []' do - expect(subject.errors).to eq([]) - end - - it 'can be set' do - shape = double('shape') - subject.errors = [shape] - expect(subject.errors).to eq([shape]) - end - - it 'can be appended to' do - shape = double('shape') - subject.errors << shape - expect(subject.errors).to eq([shape]) - end - end - end - end -end diff --git a/gems/smithy-client/spec/smithy-client/plugins/logging_spec.rb b/gems/smithy-client/spec/smithy-client/plugins/logging_spec.rb index 60208688e..e2b60e004 100644 --- a/gems/smithy-client/spec/smithy-client/plugins/logging_spec.rb +++ b/gems/smithy-client/spec/smithy-client/plugins/logging_spec.rb @@ -5,10 +5,10 @@ module Client module Plugins describe Logging do let(:client_class) do - api = API.new - api.add_operation(:operation_name, Operation.new) + schema = Schema.new + schema.add_operation(:operation_name, Schema.new) client_class = Class.new(Client::Base) - client_class.api = api + client_class.schema = schema client_class.clear_plugins client_class.add_plugin(Logging) client_class.add_plugin(DummySendPlugin) diff --git a/gems/smithy-client/spec/smithy-client/plugins/net_http_spec.rb b/gems/smithy-client/spec/smithy-client/plugins/net_http_spec.rb index c7d2276ec..fc12da2f5 100644 --- a/gems/smithy-client/spec/smithy-client/plugins/net_http_spec.rb +++ b/gems/smithy-client/spec/smithy-client/plugins/net_http_spec.rb @@ -5,10 +5,10 @@ module Client module Plugins describe NetHTTP do let(:client_class) do - api = API.new - api.add_operation(:operation_name, Operation.new) + schema = Schema.new + schema.add_operation(:operation_name, Shapes::OperationShape.new) client_class = Class.new(Client::Base) - client_class.api = api + client_class.schema = schema client_class.clear_plugins client_class.add_plugin(NetHTTP) client_class diff --git a/gems/smithy-client/spec/smithy-client/plugins/raise_response_errors_spec.rb b/gems/smithy-client/spec/smithy-client/plugins/raise_response_errors_spec.rb index 3040ce749..e7a9e1726 100644 --- a/gems/smithy-client/spec/smithy-client/plugins/raise_response_errors_spec.rb +++ b/gems/smithy-client/spec/smithy-client/plugins/raise_response_errors_spec.rb @@ -5,10 +5,10 @@ module Client module Plugins describe RaiseResponseErrors do let(:client_class) do - api = API.new - api.add_operation(:operation_name, Operation.new) + schema = Schema.new + schema.add_operation(:operation_name, Shapes::OperationShape.new) client_class = Class.new(Client::Base) - client_class.api = api + client_class.schema = schema client_class.clear_plugins client_class.add_plugin(RaiseResponseErrors) client_class.add_plugin(DummySendPlugin) diff --git a/gems/smithy-client/spec/smithy-client/plugins/response_target_spec.rb b/gems/smithy-client/spec/smithy-client/plugins/response_target_spec.rb index ab021e7f9..f6859484f 100644 --- a/gems/smithy-client/spec/smithy-client/plugins/response_target_spec.rb +++ b/gems/smithy-client/spec/smithy-client/plugins/response_target_spec.rb @@ -5,10 +5,10 @@ module Client module Plugins describe ResponseTarget do let(:client_class) do - api = API.new - api.add_operation(:operation_name, Operation.new) + schema = Schema.new + schema.add_operation(:operation_name, Shapes::OperationShape.new) client_class = Class.new(Client::Base) - client_class.api = api + client_class.schema = schema client_class.clear_plugins client_class.add_plugin(ResponseTarget) client_class.add_plugin(DummySendPlugin) diff --git a/gems/smithy-client/spec/smithy-client/schema_spec.rb b/gems/smithy-client/spec/smithy-client/schema_spec.rb new file mode 100644 index 000000000..0d371eba1 --- /dev/null +++ b/gems/smithy-client/spec/smithy-client/schema_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +module Smithy + module Client + describe Schema do + subject { Schema.new } + let(:operation_shape) { Shapes::OperationShape.new } + let(:operation_name) { :some_operation } + + it 'is enumerable' do + expect(subject).to be_kind_of(Enumerable) + end + + describe '#initialize' do + it 'yields itself' do + yielded = nil + subject = Schema.new { |schema| yielded = schema } + expect(yielded).to be(subject) + end + + it 'defaults service to nil' do + expect(subject.service).to be(nil) + end + + it 'defaults operations to empty hash' do + expect(subject.operations).to be_empty + end + end + + describe '#add_operations' do + it 'adds an operation' do + subject.add_operation(operation_name, operation_shape) + expect(subject.operations[operation_name]).to be(operation_shape) + end + end + + describe '#each' do + it 'enumerates over operations' do + subject.add_operation(operation_name, operation_shape) + expect { |b| subject.each(&b) } + .to yield_successive_args([operation_name, operation_shape]) + end + end + + describe '#inspect' do + it 'returns the class name' do + expect(subject.inspect) + .to eq('#') + end + end + + describe '#operation' do + it 'raises an ArgumentError for unknown operations' do + expect do + subject.operation(:unknown) + end.to raise_error(ArgumentError, 'unknown operation :unknown') + end + + it 'returns the operation' do + subject.add_operation(operation_name, operation_shape) + expect(subject.operation(operation_name)).to be(operation_shape) + end + end + + describe '#operation_names' do + it 'defaults to an empty array' do + expect(subject.operation_names).to eq([]) + end + + it 'provides operation names' do + subject.add_operation(operation_name, operation_shape) + subject.add_operation(:foo, Shapes::OperationShape.new) + expect(subject.operation_names) + .to eq([operation_name, :foo]) + end + end + end + end +end diff --git a/gems/smithy-client/spec/smithy-client/shapes_spec.rb b/gems/smithy-client/spec/smithy-client/shapes_spec.rb new file mode 100644 index 000000000..4e8ae0b92 --- /dev/null +++ b/gems/smithy-client/spec/smithy-client/shapes_spec.rb @@ -0,0 +1,201 @@ +# frozen_string_literal: true + +module Smithy + module Client + module Shapes + describe Shape do + subject { Shape.new } + + describe '#initialize' do + it 'defaults id to nil' do + expect(subject.id).to be(nil) + end + + it 'defaults traits to an empty hash' do + expect(subject.traits).to be_empty + end + end + end + + describe ServiceShape do + subject { ServiceShape.new } + + it 'is a subclass of Shape' do + expect(subject).to be_kind_of(Shape) + end + + describe '#initialize' do + it 'defaults version to nil' do + expect(subject.version).to be(nil) + end + + it 'version can be read when set' do + subject = ServiceShape.new(version: '2015-01-01') + expect(subject.version).to eq('2015-01-01') + end + end + end + + describe OperationShape do + subject { OperationShape.new } + + it 'is a subclass of Shape' do + expect(subject).to be_kind_of(Shape) + end + + describe '#initialize' do + let(:structure_shape) { StructureShape.new } + + context 'error attribute' do + it 'defaults to empty array' do + expect(subject.errors).to be_empty + end + + it 'can be read when set' do + subject = OperationShape.new(errors: [structure_shape]) + expect(subject.errors).to eq([structure_shape]) + end + end + + context 'input attribute' do + it 'defaults to nil' do + expect(subject.input).to be(nil) + end + + it 'can be read when set' do + subject = OperationShape.new(input: structure_shape) + expect(subject.input).to be(structure_shape) + end + end + + context 'output attribute' do + it 'defaults to nil' do + expect(subject.output).to be(nil) + end + + it 'can be read when set' do + subject = OperationShape.new(output: structure_shape) + expect(subject.output).to be(structure_shape) + end + end + end + end + + describe EnumShape do + subject { EnumShape.new } + + it 'is a subclass of Shape' do + expect(subject).to be_kind_of(Shape) + end + + describe '#initialize' do + context 'members attribute' do + it 'defaults to empty hash' do + expect(subject.members).to be_empty + end + end + end + + describe '#add_member' do + it 'adds a member' do + member_name = 'FOO' + subject.add_member(member_name, StringShape.new, {}) + expect(subject.members[member_name]).to be_kind_of(MemberShape) + end + end + end + + describe ListShape do + subject { ListShape.new } + + it 'is a subclass of Shape' do + expect(subject).to be_kind_of(Shape) + end + + describe '#initialize' do + context 'member attribute' do + it 'defaults to nil' do + expect(subject.member).to be(nil) + end + end + end + + describe '#set_member' do + it 'sets a member' do + subject.set_member(StringShape.new, {}) + expect(subject.member.name).to eq('member') + expect(subject.member).to be_kind_of(MemberShape) + end + end + end + + describe MapShape do + subject { MapShape.new } + + it 'is a subclass of Shape' do + expect(subject).to be_kind_of(Shape) + end + + describe '#initialize' do + context 'key attribute' do + it 'defaults to nil' do + expect(subject.key).to be(nil) + end + end + + context 'member value attribute' do + it 'defaults to nil' do + expect(subject.value).to be(nil) + end + end + end + + describe '#set_key' do + it 'sets a key' do + subject.set_key(StringShape.new, {}) + expect(subject.key.name).to eq('key') + expect(subject.key).to be_kind_of(MemberShape) + end + end + + describe '#set_value' do + it 'sets a value' do + subject.set_value(StringShape.new, {}) + expect(subject.value.name).to eq('value') + expect(subject.value).to be_kind_of(MemberShape) + end + end + end + + describe StructureShape do + subject { StructureShape.new } + + it 'is a subclass of Shape' do + expect(subject).to be_kind_of(Shape) + end + + describe '#initialize' do + context 'members attribute' do + it 'defaults to empty hash' do + expect(subject.members).to be_empty + end + end + + context 'type attribute' do + it 'defaults to nil' do + expect(subject.type).to be(nil) + end + end + end + + describe '#add_member' do + it 'adds a member' do + member_name = 'string_member' + subject.add_member(member_name, StringShape.new, {}) + expect(subject.members[member_name]).to be_kind_of(MemberShape) + end + end + end + end + end +end diff --git a/gems/smithy/lib/smithy/anvil/client/templates/client_class.erb b/gems/smithy/lib/smithy/anvil/client/templates/client_class.erb index 48fea932c..a18930753 100644 --- a/gems/smithy/lib/smithy/anvil/client/templates/client_class.erb +++ b/gems/smithy/lib/smithy/anvil/client/templates/client_class.erb @@ -5,7 +5,7 @@ module <%= namespace %> # TODO! class Client < Smithy::Client::Base - # self.api = API + self.schema = Shapes::SCHEMA <% plugins.each do |p| -%> add_plugin(<%= p %>) @@ -32,14 +32,14 @@ module <%= namespace %> handlers = @handlers.for(operation_name) context = Smithy::Client::HandlerContext.new( operation_name: operation_name, - operation: config.api.operation(operation_name), + operation: config.schema.operation(operation_name), client: self, params: params, - config: config + config: config, ) context[:gem_name] = '<%= gem_name %>' context[:gem_version] = '<%= gem_version %>' - Smithy::Client::Input.new(handlers, context) + Smithy::Client::Input.new(handlers: handlers, context: context) end end end diff --git a/gems/smithy/lib/smithy/anvil/client/templates/shapes.erb b/gems/smithy/lib/smithy/anvil/client/templates/shapes.erb new file mode 100644 index 000000000..10a44e6a7 --- /dev/null +++ b/gems/smithy/lib/smithy/anvil/client/templates/shapes.erb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +# This is generated code! + +module <%= namespace %> + # This module contains the shapes used by the client. + module Shapes + include Smithy::Client::Shapes + +<% shapes.each do |shape| -%> + <%= shape.name %> = <%= shape.type %>.new(id: "<%= shape.id %>", traits: <%= shape.traits %>) +<% end -%> + +<% shapes_with_members.each do |shape| -%> +<% shape.members.each do |member| -%> + <%= shape.name %>.<%= member.add_member_method(shape.type) %> +<% end -%> +<% if shape.typed -%> + <%= shape.name %>.type = Types::<%= shape.name %> +<% end -%> + +<% end -%> + SCHEMA = Smithy::Client::Schema.new do |schema| + schema.service = ServiceShape.new( + id: "<%= service_shape.id %>", + version: "<%= service_shape.version %>", + traits: <%= service_shape.traits %> + ) +<% operation_shapes.each do |shape| -%> + schema.add_operation(:<%= shape.name %>, OperationShape.new do |operation| + operation.id = "<%= shape.id %>" + operation.input = <%= shape.input %> + operation.output = <%= shape.output %> + operation.traits = <%= shape.traits %> +<% shape.errors.each do |err| -%> + operation.errors << <%= err %> +<% end -%> + end) +<% end -%> + end + end +end diff --git a/gems/smithy/lib/smithy/anvil/client/views.rb b/gems/smithy/lib/smithy/anvil/client/views.rb index b13549366..1541d708e 100644 --- a/gems/smithy/lib/smithy/anvil/client/views.rb +++ b/gems/smithy/lib/smithy/anvil/client/views.rb @@ -7,6 +7,7 @@ require_relative 'views/errors' require_relative 'views/gemspec' require_relative 'views/module' +require_relative 'views/shapes' require_relative 'views/plugins/endpoint' require_relative 'views/rubocop' require_relative 'views/specs/endpoint_provider_spec' diff --git a/gems/smithy/lib/smithy/anvil/client/views/module.rb b/gems/smithy/lib/smithy/anvil/client/views/module.rb index 11533f734..3d46f7441 100644 --- a/gems/smithy/lib/smithy/anvil/client/views/module.rb +++ b/gems/smithy/lib/smithy/anvil/client/views/module.rb @@ -38,8 +38,8 @@ def requires if @plan.type == :types [:types] else - # Order matters here - plugins must come before client - %w[plugins/endpoint client customizations errors types endpoint_parameters endpoint_provider] + # Order matters here - plugins must come before client, types and shapes must come before client + %w[plugins/endpoint types shapes client customizations errors endpoint_parameters endpoint_provider] end end end diff --git a/gems/smithy/lib/smithy/anvil/client/views/shapes.rb b/gems/smithy/lib/smithy/anvil/client/views/shapes.rb new file mode 100644 index 000000000..a1efe500f --- /dev/null +++ b/gems/smithy/lib/smithy/anvil/client/views/shapes.rb @@ -0,0 +1,233 @@ +# frozen_string_literal: true + +module Smithy + module Anvil + module Client + module Views + # @api private + class Shapes < View + def initialize(plan) + @plan = plan + @model = plan.model + @service_shape = @plan.service + @service_index = Vise::ServiceIndex.new(@model) + super() + end + + def namespace + Tools::Namespace.namespace_from_gem_name(@plan.options[:gem_name]) + end + + def operation_shapes + @service_index.operations_for(@service_shape) + .each_with_object([]) do |(k, v), arr| + arr << build_operation_shape(k, v) + end + end + + def service_shape + ServiceShape.new( + id: @service_shape.keys.first, + traits: filter_traits(@service_shape.values.first['traits']), + version: @service_shape.values.first['version'] + ) + end + + def shapes_with_members + @shapes.select do |s| + %w[EnumShape IntEnumShape ListShape MapShape StructureShape UnionShape] + .include?(s.type) + end + end + + def shapes + @shapes = + @service_index + .shapes_for(@service_shape) + .each_with_object([]) do |(k, v), arr| + next if %w[operation resource service].include?(v['type']) + + arr << build_shape(k, v) + end + end + + private + + def build_operation_shape(id, shape) + OperationShape.new( + id: id, + name: Vise::Shape.relative_id(id).underscore, + input: Vise::Shape.relative_id(shape['input']['target']), + output: Vise::Shape.relative_id(shape['output']['target']), + errors: build_error_shapes(shape['errors']), + traits: filter_traits(shape['traits']) + ) + end + + def build_error_shapes(errors) + return [] if errors.nil? + + errors.each_with_object([]) do |err, a| + a << Vise::Shape.relative_id(err['target']) + end + end + + def build_shape(id, shape) + Shape.new( + id: id, + name: Vise::Shape.relative_id(id), + type: shape_type(shape['type']), + traits: filter_traits(shape['traits']), + members: build_member_shapes(shape) + ) + end + + def build_member_shapes(shape) + case shape['type'] + when 'enum', 'intEnum', 'structure', 'union' + build_members(shape) + when 'list' + build_list_member(shape) + when 'map' + build_map_members(shape) + else + [] + end + end + + def build_members(shape) + shape['members'].each_with_object([]) do |(k, v), arr| + arr << build_member_shape(k, v['target'], v['traits']) + end + end + + def build_list_member(shape) + m_shape = shape['member'] + [] << build_member_shape('member', m_shape['target'], m_shape['traits']) + end + + def build_map_members(shape) + %w[key value].map do |m_name| + m_shape = shape[m_name] + build_member_shape(m_name, m_shape['target'], m_shape['traits']) + end + end + + def build_member_shape(name, shape, traits) + MemberShape.new( + name: name.underscore, + shape: Vise::Shape.relative_id(shape), + traits: filter_traits(traits) + ) + end + + def filter_traits(traits) + return {} unless traits + + traits.except(*OMITTED_TRAITS) + end + + def shape_type(type) + msg = "Unsupported shape type: `#{type}'" + raise ArgumentError, msg unless SHAPE_CLASSES_MAP.include?(type) + + SHAPE_CLASSES_MAP[type] + end + + # Service shape represents a slim Smithy service shape + class ServiceShape + def initialize(options = {}) + @id = options[:id] + @traits = options[:traits] + @version = options[:version] + end + + attr_reader :id, :traits, :version + end + + # Shape represents a Smithy shape + class Shape + TYPED_SHAPES = %w[StructureShape UnionShape].freeze + + def initialize(options = {}) + @id = options[:id] + @name = options[:name] + @type = options[:type] + @traits = options[:traits] + @members = options[:members] + @typed = TYPED_SHAPES.include?(@type) + end + + attr_reader :name, :id, :type, :typed, :traits, :members + end + + # Operation Shape represents Smithy operation shape + class OperationShape + def initialize(options = {}) + @id = options[:id] + @name = options[:name] + @input = options[:input] + @output = options[:output] + @errors = options[:errors] + @traits = options[:traits] + end + + attr_reader :id, :name, :input, :output, :errors, :traits + end + + # Member Shape represents members of Smithy shape + class MemberShape + def initialize(options = {}) + @name = options[:name] + @shape = options[:shape] + @traits = options[:traits] + end + + attr_reader :name, :shape, :traits + + def add_member_method(shape) + case shape + when 'ListShape' + "set_member(#{@shape}, #{@traits})" + when 'MapShape' + "set_#{@name}(#{@shape}, #{@traits})" + else + "add_member(\"#{@name}\", #{@shape}, #{@traits})" + end + end + end + + # Traits that does not affect runtime + OMITTED_TRAITS = %w[ + smithy.api#documentation + smithy.rules#endpointRuleSet + smithy.rules#endpointTests + ].freeze + + SHAPE_CLASSES_MAP = { + 'bigDecimal' => 'BigDecimal', + 'bigInteger' => 'IntegerShape', + 'blob' => 'BlobShape', + 'boolean' => 'BooleanShape', + 'byte' => 'IntegerShape', + 'double' => 'FloatShape', + 'enum' => 'EnumShape', + 'float' => 'FloatShape', + 'integer' => 'IntegerShape', + 'intEnum' => 'IntEnumShape', + 'list' => 'ListShape', + 'long' => 'IntegerShape', + 'map' => 'MapShape', + 'operation' => 'OperationShape', + 'service' => 'ServiceShape', + 'short' => 'NumberShape', + 'string' => 'StringShape', + 'structure' => 'StructureShape', + 'timestamp' => 'TimestampShape', + 'union' => 'UnionShape' + }.freeze + end + end + end + end +end diff --git a/gems/smithy/lib/smithy/anvil/client/views/types.rb b/gems/smithy/lib/smithy/anvil/client/views/types.rb index 015fefca0..1ca395dc7 100644 --- a/gems/smithy/lib/smithy/anvil/client/views/types.rb +++ b/gems/smithy/lib/smithy/anvil/client/views/types.rb @@ -20,7 +20,7 @@ def types Vise::ServiceIndex .new(@model) .shapes_for(@plan.service) - .select { |_key, shape| shape['type'] == 'structure' } + .select { |_key, shape| %w[structure union].include?(shape['type']) } .map { |id, structure| Type.new(id, structure) } end diff --git a/gems/smithy/lib/smithy/forge/client.rb b/gems/smithy/lib/smithy/forge/client.rb index 00128642f..6ae73f387 100644 --- a/gems/smithy/lib/smithy/forge/client.rb +++ b/gems/smithy/lib/smithy/forge/client.rb @@ -33,6 +33,7 @@ def source_files e.yield "lib/#{@gem_name}.rb", render_module e.yield "lib/#{@gem_name}/client.rb", render_client e.yield "lib/#{@gem_name}/customizations.rb", render_customizations + e.yield "lib/#{@gem_name}/shapes.rb", render_shapes e.yield "lib/#{@gem_name}/types.rb", render_types e.yield "lib/#{@gem_name}/errors.rb", render_errors e.yield "lib/#{@gem_name}/endpoint_parameters.rb", render_endpoint_parameters @@ -65,6 +66,10 @@ def render_customizations Anvil::Client::Views::Customizations.new.hammer end + def render_shapes + Anvil::Client::Views::Shapes.new(@plan).hammer + end + def render_types Anvil::Client::Views::Types.new(@plan).hammer end diff --git a/gems/smithy/spec/fixtures/shapes/model.json b/gems/smithy/spec/fixtures/shapes/model.json new file mode 100644 index 000000000..ca8aba074 --- /dev/null +++ b/gems/smithy/spec/fixtures/shapes/model.json @@ -0,0 +1,203 @@ +{ + "smithy": "2.0", + "shapes": { + "smithy.ruby.tests#ClientError": { + "type": "structure", + "members": { + "message": { + "target": "smithy.ruby.tests#String", + "traits": { + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#error": "client" + } + }, + "smithy.ruby.tests#Enum": { + "type": "enum", + "members": { + "CAT": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "cat" + } + }, + "DOG": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "dog" + } + } + } + }, + "smithy.ruby.tests#IntEnum": { + "type": "intEnum", + "members": { + "FOO": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": 1 + } + }, + "BAR": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": 2 + } + } + } + }, + "smithy.ruby.tests#List": { + "type": "list", + "member": { + "target": "smithy.ruby.tests#String" + } + }, + "smithy.ruby.tests#Map": { + "type": "map", + "key": { + "target": "smithy.ruby.tests#String" + }, + "value": { + "target": "smithy.api#Integer" + } + }, + "smithy.ruby.tests#Operation": { + "type": "operation", + "input": { + "target": "smithy.ruby.tests#OperationInputOutput" + }, + "output": { + "target": "smithy.ruby.tests#OperationInputOutput" + }, + "errors": [ + { + "target": "smithy.ruby.tests#ClientError" + }, + { + "target": "smithy.ruby.tests#ServerError" + } + ], + "traits": { + "smithy.api#http": { + "method": "POST", + "uri": "/operation" + } + } + }, + "smithy.ruby.tests#OperationInputOutput": { + "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" + }, + "integer": { + "target": "smithy.api#Integer" + }, + "intEnum": { + "target": "smithy.ruby.tests#IntEnum" + }, + "long": { + "target": "smithy.api#Long" + }, + "short": { + "target": "smithy.api#Short" + }, + "string": { + "target": "smithy.ruby.tests#String" + }, + "timestamp": { + "target": "smithy.api#Timestamp" + }, + "id": { + "target": "smithy.ruby.tests#String", + "traits": { + "smithy.api#required": {} + } + }, + "structure": { + "target": "smithy.ruby.tests#OperationInputOutput" + }, + "list": { + "target": "smithy.ruby.tests#List" + }, + "map": { + "target": "smithy.ruby.tests#Map" + }, + "union": { + "target": "smithy.ruby.tests#Union" + } + }, + "traits": { + "smithy.api#documentation": "This is a documentation", + "smithy.api#xmlName": "Structure" + } + }, + "smithy.ruby.tests#ServerError": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#error": "server", + "smithy.api#retryable": { + "throttling": true + } + } + }, + "smithy.ruby.tests#ShapeService": { + "type": "service", + "version": "2018-10-31", + "operations": [ + { + "target": "smithy.ruby.tests#Operation" + } + ], + "traits": { + "smithy.api#paginated": { + "inputToken": "nextToken", + "outputToken": "nextToken", + "pageSize": "pageSize" + } + } + }, + "smithy.ruby.tests#String": { + "type": "string", + "traits": { + "smithy.api#pattern": "^[A-Za-z0-9 ]+$" + } + }, + "smithy.ruby.tests#Union": { + "type": "union", + "members": { + "list": { + "target": "smithy.ruby.tests#List" + } + } + } + } +} diff --git a/gems/smithy/spec/fixtures/shapes/model.smithy b/gems/smithy/spec/fixtures/shapes/model.smithy new file mode 100644 index 000000000..e7c79a65c --- /dev/null +++ b/gems/smithy/spec/fixtures/shapes/model.smithy @@ -0,0 +1,86 @@ +$version: "2" + +namespace smithy.ruby.tests + +@paginated(inputToken: "nextToken", outputToken: "nextToken", pageSize: "pageSize") +service ShapeService { + version: "2018-10-31" + operations: [ + Operation + ] +} + +@http(method: "POST", uri: "/operation") +operation Operation { + input: OperationInputOutput + output: OperationInputOutput + errors: [ClientError, ServerError] +} + +@documentation("This is a documentation") +@xmlName("Structure") +structure OperationInputOutput { + // simple members + bigDecimal: BigDecimal + bigInteger: BigInteger + blob: Blob + boolean: Boolean + byte: Byte + document: Document + double: Double + enum: Enum + float: Float + integer: Integer + intEnum: IntEnum + long: Long + short: Short + string: String + timestamp: Timestamp + + // member with trait + @required + id: String + + // complex members + structure: OperationInputOutput + list: List + map: Map + union: Union +} + +enum Enum { + CAT = "cat" + DOG = "dog" +} + +@error("client") +structure ClientError { + @required + message: String +} + +@error("server") +@retryable(throttling: true) +structure ServerError {} + +intEnum IntEnum { + FOO = 1 + BAR = 2 +} + +list List { + member: String +} + +map Map { + key: String + value: Integer +} + +@pattern("^[A-Za-z0-9 ]+$") +string String + +union Union { + list: List +} + diff --git a/gems/smithy/spec/interfaces/client/client_spec.rb b/gems/smithy/spec/interfaces/client/client_spec.rb index 6d48a2c23..623e28135 100644 --- a/gems/smithy/spec/interfaces/client/client_spec.rb +++ b/gems/smithy/spec/interfaces/client/client_spec.rb @@ -19,10 +19,10 @@ 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) diff --git a/gems/smithy/spec/interfaces/client/shapes_spec.rb b/gems/smithy/spec/interfaces/client/shapes_spec.rb new file mode 100644 index 000000000..97959dcdc --- /dev/null +++ b/gems/smithy/spec/interfaces/client/shapes_spec.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +describe 'Component: Shapes' do + shape_tests = JSON.load_file(File.expand_path('../../fixtures/shapes/model.json', __dir__)) + + before(:all) do + @tmpdir = SpecHelper.generate(['ShapeService'], :client, fixture: 'shapes') + end + + after(:all) do + SpecHelper.cleanup(['ShapeService'], @tmpdir) + end + + let(:service) { ShapeService } + let(:types_module) { service::Types } + let(:shapes_module) { Smithy::Client::Shapes } + subject { service::Shapes } + + it 'generates a shapes module' do + expect(subject).to be_a(Module) + end + + context 'generated shapes' do + shape_tests['shapes'].each do |id, shape| + context 'a generated shape' do + next if %w[operation service].include?(shape['type']) + + let(:shape_name) { Smithy::Vise::Shape.relative_id(id) } + let(:generated_shape) { Object.const_get("#{subject}::#{shape_name}") } + + it 'is a shape of expected shape type and has an id' do + expected_shape_type = + Object.const_get("#{shapes_module}::#{shape['type'].camelize}Shape") + + expect(generated_shape).to be_a(expected_shape_type) + expect(generated_shape.id).to eq(id) + end + + it 'has a type variation of the shape when applicable' do + unless %w[structure union].include?(shape['type']) + skip("Test does not expect the generated #{id} to have a type") + end + + expected_type = Object.const_get("#{types_module}::#{shape_name}") + expect(generated_shape.type).to eq(expected_type) + end + + it 'has traits when applicable and the traits does not contain omitted traits' do + skip("Test does not expect the generated #{id} to have traits") if shape['traits'].nil? + + expected_traits = shape['traits'].reject { |t| t == 'smithy.api#documentation' } + expect(generated_shape.traits).to include(expected_traits) + expect(generated_shape.traits.keys).not_to include('smithy.api#documentation') + end + + context 'members' do + let(:m_tests) { shape['members'] } + + it 'are shapes of expected name, shape and contains traits when applicable' do + skip("Test does not expect the generated #{id} to have members") if m_tests.nil? + + m_tests.each do |m_name, m_test| + m_name = m_name.underscore + expect(generated_shape.members.keys).to include(m_name) + + generated_member_shape = generated_shape.members[m_name] + expect(generated_member_shape.name).to eq(m_name) + expect(generated_member_shape.shape.id).to eq(m_test['target']) + + if (expected_traits = m_test['traits']) + expect(generated_member_shape.traits).to include(expected_traits) + end + end + end + end + + context 'member' do + let(:m_test) { shape['member'] } + + it 'is a shape of expected member name, shape and contains traits when applicable' do + skip("Test does not expect the generated #{id} to have a member") if m_test.nil? + + expect(generated_shape.member.name).to eq('member') + expect(generated_shape.member.shape.id).to eq(m_test['target']) + + if (expected_traits = m_test['traits']) + expect(generated_shape.member.traits).to include(expected_traits) + end + end + end + + context 'key and value members' do + it 'are shapes of expected member names, shapes and contains traits when applicable' do + if shape['key'].nil? && shape['value'].nil? + skip("Test does not expect the generated #{id} to have a key/value members") + end + + expect(generated_shape.key.name).to eq('key') + expect(generated_shape.value.name).to eq('value') + expect(generated_shape.key.shape.id).to eq(shape['key']['target']) + expect(generated_shape.value.shape.id).to eq(shape['value']['target']) + + if (expected_traits = shape['key']['traits']) + expect(generated_shape.key.traits).to include(expected_traits) + end + + if (expected_traits = shape['value']['traits']) + expect(generated_shape.value.traits).to include(expected_traits) + end + end + end + end + 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) { shape_tests['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(shapes_module::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) { shape_tests.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.input.id).to eq(shape['input']) + expect(generated_shape.output.id).to eq(shape['output']) + + next unless (errors = shape['errors']) + + errors.each do |err| + generated_error = generated_shape.errors.find { |s| s.id == err } + expect(generated_error.id).to eq(err) + end + end + end + end + end +end diff --git a/projections/weather/lib/weather.rb b/projections/weather/lib/weather.rb index 9115abbf5..2bde7c5e4 100644 --- a/projections/weather/lib/weather.rb +++ b/projections/weather/lib/weather.rb @@ -9,9 +9,10 @@ module Weather end require_relative 'weather/plugins/endpoint' +require_relative 'weather/types' +require_relative 'weather/shapes' require_relative 'weather/client' require_relative 'weather/customizations' require_relative 'weather/errors' -require_relative 'weather/types' require_relative 'weather/endpoint_parameters' require_relative 'weather/endpoint_provider' diff --git a/projections/weather/lib/weather/client.rb b/projections/weather/lib/weather/client.rb index 2fe1025a4..bac8248bc 100644 --- a/projections/weather/lib/weather/client.rb +++ b/projections/weather/lib/weather/client.rb @@ -5,7 +5,7 @@ module Weather # TODO! class Client < Smithy::Client::Base - # self.api = API + self.schema = Shapes::SCHEMA add_plugin(Smithy::Client::Plugins::NetHTTP) add_plugin(Plugins::Endpoint) @@ -68,14 +68,14 @@ def build_input(operation_name, params) handlers = @handlers.for(operation_name) context = Smithy::Client::HandlerContext.new( operation_name: operation_name, - operation: config.api.operation(operation_name), + operation: config.schema.operation(operation_name), client: self, params: params, config: config ) context[:gem_name] = 'weather' context[:gem_version] = '1.0.0' - Smithy::Client::Input.new(handlers, context) + Smithy::Client::Input.new(handlers: handlers, context: context) end end end diff --git a/projections/weather/lib/weather/shapes.rb b/projections/weather/lib/weather/shapes.rb new file mode 100644 index 000000000..811cc7519 --- /dev/null +++ b/projections/weather/lib/weather/shapes.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +# This is generated code! + +module Weather + # This module contains the shapes used by the client. + module Shapes + include Smithy::Client::Shapes + + CityCoordinates = StructureShape.new(id: 'example.weather#CityCoordinates', traits: {}) + CityId = StringShape.new(id: 'example.weather#CityId', traits: { 'smithy.api#pattern' => '^[A-Za-z0-9 ]+$' }) + CitySummaries = ListShape.new(id: 'example.weather#CitySummaries', traits: {}) + CitySummary = StructureShape.new(id: 'example.weather#CitySummary', traits: { 'smithy.api#references' => [{ 'resource' => 'example.weather#City' }] }) + GetCityInput = StructureShape.new(id: 'example.weather#GetCityInput', traits: { 'smithy.api#input' => {} }) + GetCityOutput = StructureShape.new(id: 'example.weather#GetCityOutput', traits: { 'smithy.api#output' => {} }) + GetCurrentTimeOutput = StructureShape.new(id: 'example.weather#GetCurrentTimeOutput', traits: { 'smithy.api#output' => {} }) + GetForecastInput = StructureShape.new(id: 'example.weather#GetForecastInput', traits: { 'smithy.api#input' => {} }) + GetForecastOutput = StructureShape.new(id: 'example.weather#GetForecastOutput', traits: { 'smithy.api#output' => {} }) + ListCitiesInput = StructureShape.new(id: 'example.weather#ListCitiesInput', traits: { 'smithy.api#input' => {} }) + ListCitiesOutput = StructureShape.new(id: 'example.weather#ListCitiesOutput', traits: { 'smithy.api#output' => {} }) + NoSuchResource = StructureShape.new(id: 'example.weather#NoSuchResource', traits: { 'smithy.api#error' => 'client' }) + + CityCoordinates.add_member('latitude', Float, { 'smithy.api#required' => {} }) + CityCoordinates.add_member('longitude', Float, { 'smithy.api#required' => {} }) + CityCoordinates.type = Types::CityCoordinates + + CitySummaries.set_member(CitySummary, {}) + + CitySummary.add_member('city_id', CityId, { 'smithy.api#required' => {} }) + CitySummary.add_member('name', String, { 'smithy.api#required' => {} }) + CitySummary.type = Types::CitySummary + + GetCityInput.add_member('city_id', CityId, { 'smithy.api#required' => {} }) + GetCityInput.type = Types::GetCityInput + + GetCityOutput.add_member('name', String, { 'smithy.api#notProperty' => {}, 'smithy.api#required' => {} }) + GetCityOutput.add_member('coordinates', CityCoordinates, { 'smithy.api#required' => {} }) + GetCityOutput.type = Types::GetCityOutput + + GetCurrentTimeOutput.add_member('time', Timestamp, { 'smithy.api#required' => {} }) + GetCurrentTimeOutput.type = Types::GetCurrentTimeOutput + + GetForecastInput.add_member('city_id', CityId, { 'smithy.api#required' => {} }) + GetForecastInput.type = Types::GetForecastInput + + GetForecastOutput.add_member('chance_of_rain', Float, {}) + GetForecastOutput.type = Types::GetForecastOutput + + ListCitiesInput.add_member('next_token', String, {}) + ListCitiesInput.add_member('page_size', Integer, {}) + ListCitiesInput.type = Types::ListCitiesInput + + ListCitiesOutput.add_member('next_token', String, {}) + ListCitiesOutput.add_member('items', CitySummaries, { 'smithy.api#required' => {} }) + ListCitiesOutput.type = Types::ListCitiesOutput + + NoSuchResource.add_member('resource_type', String, { 'smithy.api#required' => {} }) + NoSuchResource.type = Types::NoSuchResource + + SCHEMA = Smithy::Client::Schema.new do |schema| + schema.service = ServiceShape.new( + id: 'example.weather#Weather', + version: '2006-03-01', + traits: { 'smithy.api#paginated' => { 'inputToken' => 'nextToken', 'outputToken' => 'nextToken', 'pageSize' => 'pageSize' } } + ) + schema.add_operation(:get_city, OperationShape.new do |operation| + operation.id = 'example.weather#GetCity' + operation.input = GetCityInput + operation.output = GetCityOutput + operation.traits = { 'smithy.api#readonly' => {} } + operation.errors << NoSuchResource + end) + schema.add_operation(:get_current_time, OperationShape.new do |operation| + operation.id = 'example.weather#GetCurrentTime' + operation.input = Unit + operation.output = GetCurrentTimeOutput + operation.traits = { 'smithy.api#readonly' => {} } + end) + schema.add_operation(:get_forecast, OperationShape.new do |operation| + operation.id = 'example.weather#GetForecast' + operation.input = GetForecastInput + operation.output = GetForecastOutput + operation.traits = { 'smithy.api#readonly' => {} } + end) + schema.add_operation(:list_cities, OperationShape.new do |operation| + operation.id = 'example.weather#ListCities' + operation.input = ListCitiesInput + operation.output = ListCitiesOutput + operation.traits = { 'smithy.api#paginated' => { 'items' => 'items' }, 'smithy.api#readonly' => {} } + end) + end + end +end