diff --git a/gems/smithy-cbor/lib/smithy-cbor/builder.rb b/gems/smithy-cbor/lib/smithy-cbor/builder.rb index 76443366f..bee2d7c4b 100644 --- a/gems/smithy-cbor/lib/smithy-cbor/builder.rb +++ b/gems/smithy-cbor/lib/smithy-cbor/builder.rb @@ -61,7 +61,7 @@ def structure(shape, values) member_shape = members[member_name] next unless member_shape - data[@extension.wire_name(member_shape)] = build_shape(member_shape, value) + data[@extension.legacy_wire_name(member_shape)] = build_shape(member_shape, value) end end @@ -71,12 +71,12 @@ def union(shape, values) # rubocop:disable Metrics/AbcSize data = {} if values.is_a?(Schema::Union) _name, member_shape = shape.target.member_by_type(values.class) - data[@extension.wire_name(member_shape)] = build_shape(member_shape, values.value) + data[@extension.legacy_wire_name(member_shape)] = build_shape(member_shape, values.value) else key, value = values.first if shape.target.member?(key) member_shape = shape.target.member(key) - data[@extension.wire_name(member_shape)] = build_shape(member_shape, value) + data[@extension.legacy_wire_name(member_shape)] = build_shape(member_shape, value) end end data diff --git a/gems/smithy-cbor/lib/smithy-cbor/parser.rb b/gems/smithy-cbor/lib/smithy-cbor/parser.rb index 85cf46015..b5e8d8216 100644 --- a/gems/smithy-cbor/lib/smithy-cbor/parser.rb +++ b/gems/smithy-cbor/lib/smithy-cbor/parser.rb @@ -54,7 +54,7 @@ def map(shape, values, result = nil) def structure(shape, values, result = nil) result = shape.target.type.new if result.nil? - index = @extension.member_index(shape.target) + index = @extension.wire_index(shape.target) values.each do |wire_name, value| next if value.nil? @@ -68,7 +68,7 @@ def structure(shape, values, result = nil) end def union(shape, values, result = nil) # rubocop:disable Metrics/AbcSize - index = @extension.member_index(shape.target) + index = @extension.wire_index(shape.target) values.each do |wire_name, value| next if value.nil? diff --git a/gems/smithy-json/lib/smithy-json/builder.rb b/gems/smithy-json/lib/smithy-json/builder.rb index 8bbf36e70..99d880eed 100644 --- a/gems/smithy-json/lib/smithy-json/builder.rb +++ b/gems/smithy-json/lib/smithy-json/builder.rb @@ -9,7 +9,8 @@ class Builder include Smithy::Schema::Shapes def initialize(options = {}) - @extension = options[:json_name] ? Smithy::Json::Extension : Smithy::Schema::Extension + @json_name = options[:json_name] || false + @extension = @json_name ? Smithy::Json::Extension : Smithy::Schema::Extension # TODO: removal end def build(shape, data) @@ -73,7 +74,7 @@ def structure(shape, values) member_shape = members[member_name] next unless member_shape - data[@extension.wire_name(member_shape)] = build_shape(member_shape, value) + data[wire_name(member_shape)] = build_shape(member_shape, value) end end @@ -94,16 +95,24 @@ def union(shape, values) # rubocop:disable Metrics/AbcSize data = {} if values.is_a?(Schema::Union) _name, member_shape = shape.target.member_by_type(values.class) - data[@extension.wire_name(member_shape)] = build_shape(member_shape, values.value) + data[wire_name(member_shape)] = build_shape(member_shape, values.value) else key, value = values.first if shape.target.member?(key) member_shape = shape.target.member(key) - data[@extension.wire_name(member_shape)] = build_shape(member_shape, value) + data[wire_name(member_shape)] = build_shape(member_shape, value) end end data end + + def wire_name(member_shape) + if @json_name + @extension.wire_name(member_shape) + else + @extension.legacy_wire_name(member_shape) + end + end end end end diff --git a/gems/smithy-json/lib/smithy-json/oj_engine.rb b/gems/smithy-json/lib/smithy-json/oj_engine.rb index 3061929be..fc6232b23 100644 --- a/gems/smithy-json/lib/smithy-json/oj_engine.rb +++ b/gems/smithy-json/lib/smithy-json/oj_engine.rb @@ -14,7 +14,9 @@ def load(json) end def dump(value) - Oj.dump(value, mode: :compat) + # Avoid Oj's JSON mode calling Time#to_json with keywords rejected by + # recent json gem versions while preserving the existing time format. + Oj.dump(value, { mode: :custom, time_format: :ruby }) end end end diff --git a/gems/smithy-json/lib/smithy-json/parser.rb b/gems/smithy-json/lib/smithy-json/parser.rb index 3dc25ada2..dcf6a2b1b 100644 --- a/gems/smithy-json/lib/smithy-json/parser.rb +++ b/gems/smithy-json/lib/smithy-json/parser.rb @@ -9,7 +9,8 @@ class Parser include Smithy::Schema::Shapes def initialize(options = {}) - @extension = options[:json_name] ? Smithy::Json::Extension : Smithy::Schema::Extension + @json_name = options[:json_name] || false + @extension = @json_name ? Smithy::Json::Extension : Smithy::Schema::Extension # TODO: removal end def parse(shape, bytes, result = nil) @@ -68,7 +69,7 @@ def structure(shape, values, result = nil) return if values.nil? result = shape.target.type.new if result.nil? - index = @extension.member_index(shape.target) + index = member_index(shape.target) values.each do |wire_name, value| next if value.nil? @@ -95,7 +96,7 @@ def timestamp(value) end def union(shape, values, result = nil) # rubocop:disable Metrics/AbcSize - index = @extension.member_index(shape.target) + index = member_index(shape.target) values.each do |wire_name, value| next if value.nil? @@ -115,6 +116,14 @@ def union(shape, values, result = nil) # rubocop:disable Metrics/AbcSize def sparse?(shape) @extension.sparse?(shape) end + + def member_index(shape) + if @json_name + @extension.member_index(shape) + else + @extension.wire_index(shape) + end + end end end end diff --git a/gems/smithy-schema/lib/smithy-schema/document_utils/deserializer.rb b/gems/smithy-schema/lib/smithy-schema/document_utils/deserializer.rb index 28e7dbb75..e25692e14 100644 --- a/gems/smithy-schema/lib/smithy-schema/document_utils/deserializer.rb +++ b/gems/smithy-schema/lib/smithy-schema/document_utils/deserializer.rb @@ -114,7 +114,7 @@ def union(shape, values, result = nil) # rubocop:disable Metrics/AbcSize end def wire_name(member_shape) - Smithy::Schema::Extension.wire_name(member_shape) + Smithy::Schema::Extension.legacy_wire_name(member_shape) end end end diff --git a/gems/smithy-schema/lib/smithy-schema/extension.rb b/gems/smithy-schema/lib/smithy-schema/extension.rb index c9ba91086..5d0a2c281 100644 --- a/gems/smithy-schema/lib/smithy-schema/extension.rb +++ b/gems/smithy-schema/lib/smithy-schema/extension.rb @@ -2,43 +2,353 @@ module Smithy module Schema - # Lookup helpers for protocol-agnostic serde using modeled member names. + # Cached schema metadata shared by Smithy protocol codecs. # # Raw Smithy trait data remains on +shape.traits+ and +member.traits+ with - # string keys. This module only provides generic modeled-name lookup - # helpers and memoizes shape-level indexes in metadata when that - # meaningfully avoids rebuilding them. + # string keys. This module resolves generic shape classification and + # modeled-member indexes. Protocol extensions own wire-specific metadata. # @api private + # rubocop:disable-next Metrics/ModuleLength module Extension - extend ExtensionHelpers + KEY = :schema + + SHAPE_LIST = 1 + SHAPE_MAP = 2 + SHAPE_STRUCTURE = 3 + SHAPE_UNION = 4 + SHAPE_BLOB = 5 + SHAPE_FLOAT = 6 + SHAPE_TIMESTAMP = 7 + SHAPE_BIG_DECIMAL = 8 + SHAPE_BOOLEAN = 9 + SHAPE_DOCUMENT = 10 + SHAPE_ENUM = 11 + SHAPE_INTEGER = 12 + SHAPE_INT_ENUM = 13 + SHAPE_STRING = 14 + + SHAPE_REF_BY_CLASS = { + Shapes::ListShape => SHAPE_LIST, + Shapes::MapShape => SHAPE_MAP, + Shapes::StructureShape => SHAPE_STRUCTURE, + Shapes::UnionShape => SHAPE_UNION, + Shapes::BlobShape => SHAPE_BLOB, + Shapes::FloatShape => SHAPE_FLOAT, + Shapes::TimestampShape => SHAPE_TIMESTAMP, + Shapes::BigDecimalShape => SHAPE_BIG_DECIMAL, + Shapes::BooleanShape => SHAPE_BOOLEAN, + Shapes::DocumentShape => SHAPE_DOCUMENT, + Shapes::EnumShape => SHAPE_ENUM, + Shapes::IntegerShape => SHAPE_INTEGER, + Shapes::IntEnumShape => SHAPE_INT_ENUM, + Shapes::StringShape => SHAPE_STRING + }.freeze class << self - # Returns the modeled member lookup index cached on the shape as - # +shape[:member_index]+. + # Returns the complete cached Schema metadata payload. + # + # Example: + # Extension.fetch(shape) + # # => { target_shape: Extension::SHAPE_STRUCTURE, ... } + def fetch(shape) + return shape[KEY] if shape.key?(KEY) + + shape[KEY] = + case shape + when Shapes::OperationShape + build_operation_metadata(shape) + when Shapes::StructureShape, Shapes::UnionShape + build_aggregate_metadata(shape) + when Shapes::MemberShape + build_member_metadata(shape) + else + build_shape_metadata(shape) + end + end + + # Returns the modeled wire-name lookup used by existing serde + # consumers. The index maps modeled member name to + # [ruby_member_name, member_shape, target_shape_ref]. # - # The index maps: - # - modeled member name - # - to [ruby_member_name, member_shape] + # Example: + # Extension.wire_index(shape) + # # => { 'wireName' => [:ruby_name, member, Extension::SHAPE_STRING] } + def wire_index(shape) + fetch(shape)[:wire_index] + end + + # Returns the canonical build lookup index. The index maps Ruby member + # name to [modeled_member_name, member_shape, target_shape_ref]. + # + # Example: + # Extension.member_index(shape) + # # => { ruby_name: ['wireName', member, Extension::SHAPE_STRING] } def member_index(shape) - shape[:member_index] ||= build_member_index(shape) + fetch(shape)[:member_index] + end + + # Returns a normalized reference for the target shape of +shape+. + # Bare shapes reference themselves, while member shapes reference + # their modeled target. + # + # Example: + # Extension.target_shape(member) + # # => Extension::SHAPE_STRING + def target_shape(shape) + fetch(shape)[:target_shape] + end + + # Returns [member_shape, target_shape_ref, sparse] for a list. + # + # Example: + # Extension.list_member(list) + # # => [member, Extension::SHAPE_STRING, true] + def list_member(shape) + fetch(shape)[:list_member] + end + + # Returns [member_shape, target_shape_ref] for a map key. + # + # Example: + # Extension.map_key_member(map) + # # => [member, Extension::SHAPE_STRING] + def map_key_member(shape) + fetch(shape)[:map_key_member] + end + + # Returns [member_shape, target_shape_ref, sparse] for a map value. + # + # Example: + # Extension.map_value_member(map) + # # => [member, Extension::SHAPE_STRING, false] + def map_value_member(shape) + fetch(shape)[:map_value_member] + end + + # Returns the modeled media type, when present. + # + # Example: + # Extension.media_type(shape) + # # => 'application/octet-stream' + def media_type(shape) + fetch(shape)[:media_type] + end + + # Returns whether the sensitive trait is present. + def sensitive?(shape) + fetch(shape)[:sensitive] + end + + # Returns whether the streaming trait is present. + def streaming?(shape) + fetch(shape)[:streaming] + end + + # Returns whether the requires-length trait is present. + def requires_length?(shape) + fetch(shape)[:requires_length] + end + + def endpoint_host_prefix(operation) + fetch(operation)[:endpoint_host_prefix] + end + + def request_compression_encodings(operation) + fetch(operation)[:request_compression_encodings] + end + + def checksum_required?(operation) + fetch(operation)[:checksum_required] + end + + def long_polling?(operation) + fetch(operation)[:long_polling] + end + + def unsigned_payload?(operation) + fetch(operation)[:unsigned_payload] + end + + def required_members(shape) + fetch(shape).fetch(:required_members, [].freeze) + end + + def host_label_index(shape) + fetch(shape).fetch(:host_label_index, {}.freeze) + end + + def idempotency_token_member(shape) + fetch(shape)[:idempotency_token_member] + end + + def streaming_member(shape) + fetch(shape)[:streaming_member] + end + + def streaming_member_unknown_length(shape) + fetch(shape)[:streaming_member_unknown_length] + end + + def event_stream_member(shape) + fetch(shape)[:event_stream_member] + end + + # Returns the effective timestamp format, or +:default+ when the + # model does not select one. + # + # Example: + # Extension.timestamp_format(member) + # # => 'date-time' + def timestamp_format(shape) + fetch(shape).fetch(:timestamp_format, :default) end - # Returns the modeled member name for schema lookup. - def wire_name(member) + # Returns a modeled union's unknown-member type when present. + # + # Example: + # Extension.unknown_member_type(union) + # # => Types::Unknown + def unknown_member_type(shape) + fetch(shape)[:unknown_member_type] + end + + # Iterates modeled members with separate Ruby name and member-shape + # arguments. With no block, returns the underlying enumerator. + # + # Example: + # Extension.each_member(shape) { |name, member| ... } + def each_member(shape, &block) + return shape.members.each unless block + + shape.members.each { |name, member| block.call(name, member) } + end + + # TODO: Remove after the remaining legacy deserializer migrates to a + # resolved extension index. + def legacy_wire_name(member) member.name end + # Returns whether a collection may retain nil values. + # + # Example: + # Extension.sparse?(list) + # # => true + def sparse?(shape) + shape.traits.key?('smithy.api#sparse') + end + private - def build_member_index(shape) - index = {} - shape.members.each do |name, member| - wire_name = wire_name(member) - next unless wire_name + def build_operation_metadata(operation) + traits = operation.traits + { + endpoint_host_prefix: traits.dig('smithy.api#endpoint', 'hostPrefix'), + request_compression_encodings: traits.dig('smithy.api#requestCompression', 'encodings'), + checksum_required: traits.key?('smithy.api#httpChecksumRequired') || nil, + long_polling: traits.key?('smithy.api#longPoll') || nil, + unsigned_payload: traits.key?('aws.auth#unsignedPayload') || nil + }.compact.freeze + end + + def build_shape_metadata(shape) + target = shape.target + target_shape = SHAPE_REF_BY_CLASS[target.class] + metadata = { target_shape: target_shape }.compact + add_collection_metadata(metadata, shape) if target.equal?(shape) + add_media_type_metadata(metadata, shape) + add_boolean_trait_metadata(metadata, shape) + add_timestamp_metadata(metadata, shape) + metadata[:unknown_member_type] = shape.member_type(:unknown) if + target_shape == SHAPE_UNION && shape.member_type?(:unknown) + metadata.freeze + end - index[wire_name] = [name, member] + def build_member_metadata(member) + target_shape = SHAPE_REF_BY_CLASS[member.target.class] if member.target + metadata = { target_shape: target_shape }.compact + add_media_type_metadata(metadata, member) + add_boolean_trait_metadata(metadata, member) + add_timestamp_metadata(metadata, member) + metadata.freeze + end + + # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity + def build_aggregate_metadata(shape) + metadata = build_shape_metadata(shape).dup + wire_index = {} + member_index = {} + required_members = [] + host_label_index = {} + + shape.members.each do |ruby_name, member| + modeled_name = member.name + next unless modeled_name + + target_shape = fetch(member)[:target_shape] + wire_index[modeled_name] = [ruby_name, member, target_shape].freeze + member_index[ruby_name] = [modeled_name, member, target_shape].freeze + if member.traits.key?('smithy.api#required') && + !member.traits.key?('smithy.api#clientOptional') + required_members << ruby_name + end + host_label_index[modeled_name] = ruby_name if member.traits.key?('smithy.api#hostLabel') + metadata[:idempotency_token_member] ||= ruby_name if member.traits.key?('smithy.api#idempotencyToken') + next unless streaming_trait?(member.target) + + metadata[:streaming_member] ||= member + metadata[:event_stream_member] ||= member if target_shape == SHAPE_UNION + metadata[:streaming_member_unknown_length] ||= member unless requires_length_trait?(member.target) end - index.freeze + + metadata[:wire_index] = wire_index.freeze + metadata[:member_index] = member_index.freeze + metadata[:required_members] = required_members.freeze + metadata[:host_label_index] = host_label_index.freeze + metadata.freeze + end + + def add_collection_metadata(metadata, shape) + case shape + when Shapes::ListShape + metadata[:list_member] = member_metadata(shape.member, sparse?(shape)) + when Shapes::MapShape + metadata[:map_key_member] = member_metadata(shape.key) + metadata[:map_value_member] = member_metadata(shape.value, sparse?(shape)) + end + end + + def add_timestamp_metadata(metadata, shape) + return unless metadata[:target_shape] == SHAPE_TIMESTAMP + + metadata[:timestamp_format] = + shape.traits['smithy.api#timestampFormat'] || + shape.target.traits['smithy.api#timestampFormat'] || + :default + end + + def add_media_type_metadata(metadata, shape) + media_type = shape.traits['smithy.api#mediaType'] + metadata[:media_type] = media_type if media_type + end + + def add_boolean_trait_metadata(metadata, shape) + metadata[:sensitive] = true if shape.traits.key?('smithy.api#sensitive') + metadata[:streaming] = true if streaming_trait?(shape) + metadata[:requires_length] = true if requires_length_trait?(shape) + end + + def streaming_trait?(shape) + shape.traits.key?('smithy.api#streaming') + end + + def requires_length_trait?(shape) + shape.traits.key?('smithy.api#requiresLength') + end + + def member_metadata(member, sparse = nil) + target_shape = target_shape(member) if member + [member, target_shape, sparse].compact.freeze end end end diff --git a/gems/smithy-schema/lib/smithy-schema/shapes.rb b/gems/smithy-schema/lib/smithy-schema/shapes.rb index 75fcedee5..ece5abeee 100644 --- a/gems/smithy-schema/lib/smithy-schema/shapes.rb +++ b/gems/smithy-schema/lib/smithy-schema/shapes.rb @@ -32,6 +32,12 @@ def [](key) @metadata[key] end + # @param [Symbol] key + # @return [Boolean] + def key?(key) + @metadata.key?(key) + end + # @param [Symbol] key # @param [Object] value def []=(key, value) @@ -62,6 +68,12 @@ def [](key) @metadata[key] end + # @param [Symbol] key + # @return [Boolean] + def key?(key) + @metadata.key?(key) + end + # @param [Symbol] key # @param [Object] value def []=(key, value) @@ -212,6 +224,10 @@ def member(name) class ListShape < Shape # @return [MemberShape] attr_accessor :member + + def add_member(_name, member_shape) + @member = member_shape + end end # Represents a Map shape. @@ -221,6 +237,13 @@ class MapShape < Shape # @return [MemberShape] attr_accessor :value + + def add_member(name, member_shape) + case name + when :key then @key = member_shape + when :value then @value = member_shape + end + end end # Represents a String shape. diff --git a/gems/smithy-schema/sig/smithy-schema/extension.rbs b/gems/smithy-schema/sig/smithy-schema/extension.rbs index 49f76edcc..3b79cac15 100644 --- a/gems/smithy-schema/sig/smithy-schema/extension.rbs +++ b/gems/smithy-schema/sig/smithy-schema/extension.rbs @@ -1,9 +1,33 @@ module Smithy module Schema module Extension - def self.member_index: (untyped shape) -> Hash[String, [Symbol, Shapes::MemberShape]] - def self.wire_name: (Shapes::MemberShape member) -> String? - def self.sparse?: (untyped shape) -> bool + def self.fetch: ((Shapes::Shape | Shapes::MemberShape) shape) -> Hash[Symbol, untyped] + def self.wire_index: ((Shapes::StructureShape | Shapes::UnionShape) shape) -> Hash[String, [Symbol, Shapes::MemberShape, Integer?]] + def self.member_index: ((Shapes::StructureShape | Shapes::UnionShape) shape) -> Hash[Symbol, [String, Shapes::MemberShape, Integer?]] + def self.target_shape: ((Shapes::Shape | Shapes::MemberShape) shape) -> Integer? + def self.list_member: (Shapes::ListShape shape) -> Array[untyped] + def self.map_key_member: (Shapes::MapShape shape) -> Array[untyped] + def self.map_value_member: (Shapes::MapShape shape) -> Array[untyped] + def self.media_type: ((Shapes::Shape | Shapes::MemberShape) shape) -> String? + def self.sensitive?: ((Shapes::Shape | Shapes::MemberShape) shape) -> bool? + def self.streaming?: ((Shapes::Shape | Shapes::MemberShape) shape) -> bool? + def self.requires_length?: ((Shapes::Shape | Shapes::MemberShape) shape) -> bool? + def self.endpoint_host_prefix: (Shapes::OperationShape operation) -> String? + def self.request_compression_encodings: (Shapes::OperationShape operation) -> Array[String]? + def self.checksum_required?: (Shapes::OperationShape operation) -> bool? + def self.long_polling?: (Shapes::OperationShape operation) -> bool? + def self.unsigned_payload?: (Shapes::OperationShape operation) -> bool? + def self.required_members: ((Shapes::StructureShape | Shapes::UnionShape) shape) -> Array[Symbol] + def self.host_label_index: ((Shapes::StructureShape | Shapes::UnionShape) shape) -> Hash[String, Symbol] + def self.idempotency_token_member: ((Shapes::StructureShape | Shapes::UnionShape) shape) -> Symbol? + def self.streaming_member: ((Shapes::StructureShape | Shapes::UnionShape) shape) -> Shapes::MemberShape? + def self.streaming_member_unknown_length: ((Shapes::StructureShape | Shapes::UnionShape) shape) -> Shapes::MemberShape? + def self.event_stream_member: ((Shapes::StructureShape | Shapes::UnionShape) shape) -> Shapes::MemberShape? + def self.timestamp_format: ((Shapes::Shape | Shapes::MemberShape) shape) -> (String | Symbol) + def self.unknown_member_type: (Shapes::UnionShape shape) -> untyped + def self.each_member: ((Shapes::StructureShape | Shapes::UnionShape) shape) ?{ (Symbol, Shapes::MemberShape) -> void } -> untyped + def self.legacy_wire_name: (Shapes::MemberShape member) -> String? + def self.sparse?: ((Shapes::Shape | Shapes::MemberShape) shape) -> bool end end end diff --git a/gems/smithy-schema/sig/smithy-schema/shapes.rbs b/gems/smithy-schema/sig/smithy-schema/shapes.rbs index bedf1d5fe..38ed6e212 100644 --- a/gems/smithy-schema/sig/smithy-schema/shapes.rbs +++ b/gems/smithy-schema/sig/smithy-schema/shapes.rbs @@ -9,6 +9,7 @@ module Smithy attr_accessor name: String attr_accessor traits: Hash[String, untyped] def []: (Symbol) -> Object + def key?: (Symbol) -> bool def []=: (Symbol, Object) -> void end @@ -19,6 +20,7 @@ module Smithy attr_accessor name: String? attr_accessor traits: Hash[String, untyped] def []: (Symbol) -> Object + def key?: (Symbol) -> bool def []=: (Symbol, Object) -> void end @@ -71,12 +73,14 @@ module Smithy end class ListShape < Shape - attr_accessor member: MemberShape + attr_accessor member: MemberShape? + def add_member: (Symbol, MemberShape) -> MemberShape end class MapShape < Shape - attr_accessor key: MemberShape - attr_accessor value: MemberShape + attr_accessor key: MemberShape? + attr_accessor value: MemberShape? + def add_member: (Symbol, MemberShape) -> MemberShape? end class StringShape < Shape diff --git a/gems/smithy-schema/spec/smithy-schema/extension_spec.rb b/gems/smithy-schema/spec/smithy-schema/extension_spec.rb index ea18aa533..0b554ed40 100644 --- a/gems/smithy-schema/spec/smithy-schema/extension_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/extension_spec.rb @@ -5,31 +5,43 @@ module Smithy module Schema describe Extension do - describe '.member_index' do + describe '.wire_index' do let(:shape) { Shapes::StructureShape.new } let(:member) { Shapes::MemberShape.new(target: Shapes::StringShape.new, name: 'wireName') } it 'returns a frozen member index keyed by member name' do shape.add_member(:some_member, member) - expect(described_class.member_index(shape)).to eq('wireName' => [:some_member, member]) - expect(described_class.member_index(shape)).to be_frozen + expected_values = [:some_member, member, described_class::SHAPE_STRING] + expect(described_class.wire_index(shape)).to eq('wireName' => expected_values) + expect(described_class.wire_index(shape)).to be_frozen end it 'ignores members that do not have a modeled member name' do shape.add_member(:missing_name, Shapes::MemberShape.new(target: Shapes::StringShape.new)) - expect(described_class.member_index(shape)).to eq({}) + expect(described_class.wire_index(shape)).to eq({}) end it 'memoizes the index on the shape metadata' do shape.add_member(:some_member, member) - expect(described_class.member_index(shape)).to be(described_class.member_index(shape)) + expect(described_class.wire_index(shape)).to be(described_class.wire_index(shape)) + end + end + + describe '.member_index' do + it 'returns a frozen member index keyed by Ruby member name' do + member = Shapes::MemberShape.new(target: Shapes::StringShape.new, name: 'wireName') + shape = Shapes::StructureShape.new + shape.add_member(:some_member, member) + + expected_values = ['wireName', member, described_class::SHAPE_STRING] + expect(described_class.member_index(shape)).to eq(some_member: expected_values) end end - describe '.wire_name' do + describe '.legacy_wire_name' do it 'returns the model name' do member = Shapes::MemberShape.new( target: Shapes::StringShape.new, @@ -37,19 +49,59 @@ module Schema traits: { 'smithy.api#jsonName' => 'jsonWireName' } ) - expect(described_class.wire_name(member)).to eq('wireName') + expect(described_class.legacy_wire_name(member)).to eq('wireName') end end describe '.sparse?' do - it 'returns true when the sparse trait is present' do - shape = Shapes::ListShape.new(traits: { 'smithy.api#sparse' => {} }) + it 'returns whether the sparse trait is present' do + expect(described_class.sparse?(Shapes::ListShape.new)).to be(false) + expect(described_class.sparse?(Shapes::ListShape.new(traits: { 'smithy.api#sparse' => {} }))).to be(true) + end + end - expect(described_class.sparse?(shape)).to be(true) + describe 'generic shape metadata' do + it 'classifies target shapes' do + expect(described_class.target_shape(Shapes::BlobShape.new)).to eq(described_class::SHAPE_BLOB) + expect(described_class.target_shape(Shapes::FloatShape.new)).to eq(described_class::SHAPE_FLOAT) + expect(described_class.target_shape(Shapes::ListShape.new)).to eq(described_class::SHAPE_LIST) end - it 'returns false when the sparse trait is absent' do - expect(described_class.sparse?(Shapes::ListShape.new)).to be(false) + it 'caches collection members and sparse metadata' do + list = Shapes::ListShape.new(traits: { 'smithy.api#sparse' => {} }) + member = Shapes::MemberShape.new(target: Shapes::StringShape.new) + list.add_member(:member, member) + + expect(described_class.list_member(list)).to eq([member, described_class::SHAPE_STRING, true]) + end + + it 'resolves a member timestamp format before its target format' do + timestamp = Shapes::TimestampShape.new( + traits: { 'smithy.api#timestampFormat' => 'date-time' } + ) + member = Shapes::MemberShape.new( + target: timestamp, + traits: { 'smithy.api#timestampFormat' => 'http-date' } + ) + + expect(described_class.timestamp_format(member)).to eq('http-date') + expect(described_class.timestamp_format(Shapes::TimestampShape.new)).to eq(:default) + end + + it 'caches a modeled media type using the Smithy trait key' do + shape = Shapes::BlobShape.new( + traits: { 'smithy.api#mediaType' => 'application/custom' } + ) + + expect(described_class.media_type(shape)).to eq('application/custom') + end + + it 'caches an unknown union member type when present' do + union = Shapes::UnionShape.new + unknown_type = Class.new + union.add_member(:unknown, unknown_type, Shapes::MemberShape.new) + + expect(described_class.unknown_member_type(union)).to be(unknown_type) end end end diff --git a/gems/smithy-schema/spec/spec_helper.rb b/gems/smithy-schema/spec/spec_helper.rb index a42d47ea6..1cc21da28 100644 --- a/gems/smithy-schema/spec/spec_helper.rb +++ b/gems/smithy-schema/spec/spec_helper.rb @@ -2,12 +2,12 @@ require 'simplecov' SimpleCov.start do - add_filter '/spec/' - add_filter 'gems/smithy/' - add_filter 'gems/smithy-cbor/' - add_filter 'gems/smithy-client/' - add_filter 'gems/smithy-json/' - add_filter 'gems/smithy-xml/' + skip '/spec/' + skip 'gems/smithy/' + skip 'gems/smithy-cbor/' + skip 'gems/smithy-client/' + skip 'gems/smithy-json/' + skip 'gems/smithy-xml/' end require 'smithy' diff --git a/gems/smithy/lib/smithy/templates/client/schema.erb b/gems/smithy/lib/smithy/templates/client/schema.erb index 4c4bd1251..8627474de 100644 --- a/gems/smithy/lib/smithy/templates/client/schema.erb +++ b/gems/smithy/lib/smithy/templates/client/schema.erb @@ -17,10 +17,10 @@ module <%= module_name %> <%= shape.name %>.add_member(:<%= member.ruby_name %>, <%= member.initializer %>) <% end -%> <% when 'list' -%> - <%= shape.name %>.member = <%= shape.member.initializer %> + <%= shape.name %>.add_member(:member, <%= shape.member.initializer %>) <% when 'map' -%> - <%= shape.name %>.key = <%= shape.key.initializer %> - <%= shape.name %>.value = <%= shape.value.initializer %> + <%= shape.name %>.add_member(:key, <%= shape.key.initializer %>) + <%= shape.name %>.add_member(:value, <%= shape.value.initializer %>) <% when 'structure' -%> <% shape.members.each do |member| -%> <%= shape.name %>.add_member(:<%= member.ruby_name %>, <%= member.initializer %>) diff --git a/projections/shapes/lib/shapes/schema.rb b/projections/shapes/lib/shapes/schema.rb index 6a807dec5..464aa2e09 100644 --- a/projections/shapes/lib/shapes/schema.rb +++ b/projections/shapes/lib/shapes/schema.rb @@ -30,9 +30,9 @@ module Schema Enum.add_member(:foo, ::Smithy::Schema::Shapes::MemberShape.new(target: ::Smithy::Schema::Shapes::Prelude::Unit, name: "FOO", traits: {"smithy.api#enumValue" => "bar"})) IntEnum.add_member(:baz, ::Smithy::Schema::Shapes::MemberShape.new(target: ::Smithy::Schema::Shapes::Prelude::Unit, name: "BAZ", traits: {"smithy.api#enumValue" => 1})) - List.member = ::Smithy::Schema::Shapes::MemberShape.new(target: String, name: "member", traits: {"smithy.ruby.tests#shape" => {}}) - Map.key = ::Smithy::Schema::Shapes::MemberShape.new(target: String, name: "key", traits: {"smithy.ruby.tests#shape" => {}}) - Map.value = ::Smithy::Schema::Shapes::MemberShape.new(target: String, name: "value", traits: {"smithy.ruby.tests#shape" => {}}) + List.add_member(:member, ::Smithy::Schema::Shapes::MemberShape.new(target: String, name: "member", traits: {"smithy.ruby.tests#shape" => {}})) + Map.add_member(:key, ::Smithy::Schema::Shapes::MemberShape.new(target: String, name: "key", traits: {"smithy.ruby.tests#shape" => {}})) + Map.add_member(:value, ::Smithy::Schema::Shapes::MemberShape.new(target: String, name: "value", traits: {"smithy.ruby.tests#shape" => {}})) OperationInput.add_member(:blob, ::Smithy::Schema::Shapes::MemberShape.new(target: Blob, name: "blob")) OperationInput.add_member(:boolean, ::Smithy::Schema::Shapes::MemberShape.new(target: Boolean, name: "boolean")) OperationInput.add_member(:string, ::Smithy::Schema::Shapes::MemberShape.new(target: String, name: "string")) diff --git a/projections/weather/lib/weather/schema.rb b/projections/weather/lib/weather/schema.rb index 40c7c2248..cb1a1a078 100644 --- a/projections/weather/lib/weather/schema.rb +++ b/projections/weather/lib/weather/schema.rb @@ -22,7 +22,7 @@ module Schema CityCoordinates.add_member(:latitude, ::Smithy::Schema::Shapes::MemberShape.new(target: ::Smithy::Schema::Shapes::Prelude::Float, name: "latitude", traits: {"smithy.api#required" => {}})) CityCoordinates.add_member(:longitude, ::Smithy::Schema::Shapes::MemberShape.new(target: ::Smithy::Schema::Shapes::Prelude::Float, name: "longitude", traits: {"smithy.api#required" => {}})) CityCoordinates.type = Types::CityCoordinates - CitySummaries.member = ::Smithy::Schema::Shapes::MemberShape.new(target: CitySummary, name: "member") + CitySummaries.add_member(:member, ::Smithy::Schema::Shapes::MemberShape.new(target: CitySummary, name: "member")) CitySummary.add_member(:city_id, ::Smithy::Schema::Shapes::MemberShape.new(target: CityId, name: "cityId", traits: {"smithy.api#required" => {}})) CitySummary.add_member(:name, ::Smithy::Schema::Shapes::MemberShape.new(target: ::Smithy::Schema::Shapes::Prelude::String, name: "name", traits: {"smithy.api#required" => {}})) CitySummary.type = Types::CitySummary diff --git a/tasks/smithy-cbor.rake b/tasks/smithy-cbor.rake index f20517ff0..4c824fb5f 100644 --- a/tasks/smithy-cbor.rake +++ b/tasks/smithy-cbor.rake @@ -13,7 +13,7 @@ namespace 'smithy-cbor' do desc 'Run RBS validation.' task 'rbs:validate' do - sh('bundle exec rbs -I gems/smithy-cbor/sig -I gems/smithy-schema/sig validate') + sh('bundle exec rbs -r delegate -I gems/smithy-cbor/sig -I gems/smithy-schema/sig validate') end desc 'Run RBS spy tests on all unit tests.' @@ -22,7 +22,7 @@ namespace 'smithy-cbor' do 'RUBYOPT' => '-r bundler/setup -r rbs/test/setup', 'RBS_TEST_RAISE' => 'true', 'RBS_TEST_LOGLEVEL' => 'error', - 'RBS_TEST_OPT' => '-I gems/smithy-cbor/sig -I gems/smithy-schema/sig', + 'RBS_TEST_OPT' => '-r delegate -I gems/smithy-cbor/sig -I gems/smithy-schema/sig', 'RBS_TEST_TARGET' => '"Smithy,Smithy::*,Smithy::Cbor,Smithy::Cbor::*"', 'RBS_TEST_DOUBLE_SUITE' => 'rspec' } diff --git a/tasks/smithy-client.rake b/tasks/smithy-client.rake index 20185ce9c..c99bf6442 100644 --- a/tasks/smithy-client.rake +++ b/tasks/smithy-client.rake @@ -14,7 +14,7 @@ namespace 'smithy-client' do desc 'Run RBS validation.' task 'rbs:validate' do - sh('bundle exec rbs -I gems/smithy-client/sig -I gems/smithy-schema/sig validate') + sh('bundle exec rbs -r delegate -r uri -I gems/smithy-client/sig -I gems/smithy-schema/sig validate') end desc 'Run RBS spy tests on all unit tests.' @@ -23,7 +23,7 @@ namespace 'smithy-client' do 'RUBYOPT' => '-r bundler/setup -r rbs/test/setup', 'RBS_TEST_RAISE' => 'true', 'RBS_TEST_LOGLEVEL' => 'error', - 'RBS_TEST_OPT' => '-I gems/smithy-client/sig -I gems/smithy-schema/sig', + 'RBS_TEST_OPT' => '-r delegate -r uri -I gems/smithy-client/sig -I gems/smithy-schema/sig', 'RBS_TEST_TARGET' => '"Smithy,Smithy::*,Smithy::Client,Smithy::Client::*"', 'RBS_TEST_DOUBLE_SUITE' => 'rspec' } diff --git a/tasks/smithy-json.rake b/tasks/smithy-json.rake index a959d4568..82c45d08c 100644 --- a/tasks/smithy-json.rake +++ b/tasks/smithy-json.rake @@ -13,7 +13,7 @@ namespace 'smithy-json' do desc 'Run RBS validation.' task 'rbs:validate' do - sh('bundle exec rbs -I gems/smithy-json/sig -I gems/smithy-schema/sig validate') + sh('bundle exec rbs -r delegate -I gems/smithy-json/sig -I gems/smithy-schema/sig validate') end desc 'Run RBS spy tests on all unit tests.' @@ -22,7 +22,7 @@ namespace 'smithy-json' do 'RUBYOPT' => '-r bundler/setup -r rbs/test/setup', 'RBS_TEST_RAISE' => 'true', 'RBS_TEST_LOGLEVEL' => 'error', - 'RBS_TEST_OPT' => '-I gems/smithy-json/sig -I gems/smithy-schema/sig', + 'RBS_TEST_OPT' => '-r delegate -I gems/smithy-json/sig -I gems/smithy-schema/sig', 'RBS_TEST_TARGET' => '"Smithy,Smithy::*,Smithy::Json,Smithy::Json::*"', 'RBS_TEST_DOUBLE_SUITE' => 'rspec' } diff --git a/tasks/smithy-schema.rake b/tasks/smithy-schema.rake index c43092dae..7c852131a 100644 --- a/tasks/smithy-schema.rake +++ b/tasks/smithy-schema.rake @@ -13,7 +13,7 @@ namespace 'smithy-schema' do desc 'Run RBS validation.' task 'rbs:validate' do - sh('bundle exec rbs -I gems/smithy-schema/sig validate') + sh('bundle exec rbs -r delegate -I gems/smithy-schema/sig validate') end desc 'Run RBS spy tests on all unit tests.' diff --git a/tasks/smithy-xml.rake b/tasks/smithy-xml.rake index 0cd0febc5..08bbb2ab1 100644 --- a/tasks/smithy-xml.rake +++ b/tasks/smithy-xml.rake @@ -13,7 +13,7 @@ namespace 'smithy-xml' do desc 'Run RBS validation.' task 'rbs:validate' do - sh('bundle exec rbs -I gems/smithy-xml/sig -I gems/smithy-schema/sig validate') + sh('bundle exec rbs -r delegate -I gems/smithy-xml/sig -I gems/smithy-schema/sig validate') end desc 'Run RBS spy tests on all unit tests.' @@ -22,7 +22,7 @@ namespace 'smithy-xml' do 'RUBYOPT' => '-r bundler/setup -r rbs/test/setup', 'RBS_TEST_RAISE' => 'true', 'RBS_TEST_LOGLEVEL' => 'error', - 'RBS_TEST_OPT' => '-I gems/smithy-xml/sig -I gems/smithy-schema/sig', + 'RBS_TEST_OPT' => '-r delegate -I gems/smithy-xml/sig -I gems/smithy-schema/sig', 'RBS_TEST_TARGET' => '"Smithy,Smithy::*,Smithy::Xml,Smithy::Xml::*"', 'RBS_TEST_DOUBLE_SUITE' => 'rspec' }