From 56fa5e80d5fe9223afedcae5ceb6c80b89efad9e Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 14 Sep 2026 10:57:42 -0700 Subject: [PATCH 01/36] perf: reuse cached helpers across serde codecs --- gems/smithy-cbor/lib/smithy-cbor/builder.rb | 3 +- gems/smithy-cbor/lib/smithy-cbor/codec.rb | 7 ++- .../spec/smithy-cbor/codec_spec.rb | 33 ++++++++++ .../lib/smithy-client/log_param_filter.rb | 18 +++--- gems/smithy-json/lib/smithy-json/builder.rb | 3 +- gems/smithy-json/lib/smithy-json/codec.rb | 7 ++- .../spec/smithy-json/codec_spec.rb | 33 ++++++++++ gems/smithy-xml/lib/smithy-xml/builder.rb | 63 ++++++++++--------- gems/smithy-xml/lib/smithy-xml/codec.rb | 7 ++- .../smithy-xml/lib/smithy-xml/parser/frame.rb | 8 +-- .../spec/smithy-xml/builder_spec.rb | 56 +++++++++++++++++ gems/smithy-xml/spec/smithy-xml/codec_spec.rb | 33 ++++++++++ 12 files changed, 215 insertions(+), 56 deletions(-) create mode 100644 gems/smithy-cbor/spec/smithy-cbor/codec_spec.rb create mode 100644 gems/smithy-json/spec/smithy-json/codec_spec.rb create mode 100644 gems/smithy-xml/spec/smithy-xml/codec_spec.rb diff --git a/gems/smithy-cbor/lib/smithy-cbor/builder.rb b/gems/smithy-cbor/lib/smithy-cbor/builder.rb index d8de7d0e8..f43928891 100644 --- a/gems/smithy-cbor/lib/smithy-cbor/builder.rb +++ b/gems/smithy-cbor/lib/smithy-cbor/builder.rb @@ -67,8 +67,7 @@ def union(shape, values) key, value = if values.is_a?(Schema::Union) - member_name, _member_shape = shape.target.member_by_type(values.class) - [member_name, values.value] + [values.member, values.value] else values.first end diff --git a/gems/smithy-cbor/lib/smithy-cbor/codec.rb b/gems/smithy-cbor/lib/smithy-cbor/codec.rb index 2c95508e0..b1d9b7042 100644 --- a/gems/smithy-cbor/lib/smithy-cbor/codec.rb +++ b/gems/smithy-cbor/lib/smithy-cbor/codec.rb @@ -6,14 +6,15 @@ module Cbor class Codec # @param [Hash] options def initialize(options = {}) - @options = options + @builder = Builder.new(options) + @parser = Parser.new(options) end # @param [Shape] shape # @param [Object] data # @return [String, nil] def build(shape, data) - Builder.new(@options).build(shape, data) + @builder.build(shape, data) end # @param [Shape] shape @@ -21,7 +22,7 @@ def build(shape, data) # @param [Object, nil] result (nil) # @return [Object, nil] def parse(shape, bytes, result = nil) - Parser.new(@options).parse(shape, bytes, result) + @parser.parse(shape, bytes, result) end end end diff --git a/gems/smithy-cbor/spec/smithy-cbor/codec_spec.rb b/gems/smithy-cbor/spec/smithy-cbor/codec_spec.rb new file mode 100644 index 000000000..350973f51 --- /dev/null +++ b/gems/smithy-cbor/spec/smithy-cbor/codec_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require_relative '../spec_helper' + +module Smithy + module Cbor + describe Codec do + let(:shapes) { SchemaHelper.sample_shapes } + let(:sample_schema) { SchemaHelper.sample_schema(shapes: shapes) } + let(:structure_shape) { sample_schema.const_get(:Structure) } + + it 'reuses the same codec instance across build calls without leaking builder state' do + codec = described_class.new + + first = codec.build(structure_shape, { string: 'first' }) + second = codec.build(structure_shape, { integer: 123 }) + + expect(Cbor.decode(first)).to eq('string' => 'first') + expect(Cbor.decode(second)).to eq('integer' => 123) + end + + it 'reuses the same codec instance across parse calls' do + codec = described_class.new + + first = codec.parse(structure_shape, Cbor.encode('string' => 'first')) + second = codec.parse(structure_shape, Cbor.encode('integer' => 123)) + + expect(first.to_h).to eq(string: 'first') + expect(second.to_h).to eq(integer: 123) + end + end + end +end diff --git a/gems/smithy-client/lib/smithy-client/log_param_filter.rb b/gems/smithy-client/lib/smithy-client/log_param_filter.rb index d5b0697dc..53a444ea8 100644 --- a/gems/smithy-client/lib/smithy-client/log_param_filter.rb +++ b/gems/smithy-client/lib/smithy-client/log_param_filter.rb @@ -4,18 +4,16 @@ module Smithy module Client # @api private class LogParamFilter - include Schema::Shapes - def initialize(options = {}) @filter_sensitive_params = options.fetch(:filter_sensitive_params, true) end def filter(shape, values) - case shape.target - when ListShape then list(shape, values) - when MapShape then map(shape, values) - when StructureShape then structure(shape, values) - when UnionShape then union(shape, values) + case Schema::Extension.target_shape(shape) + when Schema::Extension::SHAPE_LIST then list(shape, values) + when Schema::Extension::SHAPE_MAP then map(shape, values) + when Schema::Extension::SHAPE_STRUCTURE then structure(shape, values) + when Schema::Extension::SHAPE_UNION then union(shape, values) else scalar(shape, values) end end @@ -26,7 +24,7 @@ def list(shape, values) target = shape.target return '[FILTERED]' if sensitive?(target) - member = target.member + member, = Schema::Extension.list_member(target) values.collect { |value| filter(member, value) } end @@ -35,7 +33,7 @@ def map(shape, values) return '[FILTERED]' if sensitive?(target) filtered = {} - value_shape = target.value + value_shape, = Schema::Extension.map_value_member(target) values.each_pair do |key, value| filtered[key] = filter(value_shape, value) end @@ -81,7 +79,7 @@ def union(shape, values) end def sensitive?(shape) - @filter_sensitive_params && shape.traits.key?('smithy.api#sensitive') + @filter_sensitive_params && Schema::Extension.sensitive?(shape) end end end diff --git a/gems/smithy-json/lib/smithy-json/builder.rb b/gems/smithy-json/lib/smithy-json/builder.rb index bbdbba2d4..21b902a87 100644 --- a/gems/smithy-json/lib/smithy-json/builder.rb +++ b/gems/smithy-json/lib/smithy-json/builder.rb @@ -95,8 +95,7 @@ def union(shape, values) key, value = if values.is_a?(Schema::Union) - member_name, _member_shape = shape.target.member_by_type(values.class) - [member_name, values.value] + [values.member, values.value] else values.first end diff --git a/gems/smithy-json/lib/smithy-json/codec.rb b/gems/smithy-json/lib/smithy-json/codec.rb index 0ad2c6d3c..d67a1ea5f 100644 --- a/gems/smithy-json/lib/smithy-json/codec.rb +++ b/gems/smithy-json/lib/smithy-json/codec.rb @@ -6,14 +6,15 @@ module Json class Codec # @param [Hash] options def initialize(options = {}) - @options = options + @builder = Builder.new(options) + @parser = Parser.new(options) end # @param [Shape] shape # @param [Object] data # @return [String, nil] def build(shape, data) - Builder.new(@options).build(shape, data) + @builder.build(shape, data) end # @param [Shape] shape @@ -21,7 +22,7 @@ def build(shape, data) # @param [Object, nil] result (nil) # @return [Object, nil] def parse(shape, bytes, result = nil) - Parser.new(@options).parse(shape, bytes, result) + @parser.parse(shape, bytes, result) end end end diff --git a/gems/smithy-json/spec/smithy-json/codec_spec.rb b/gems/smithy-json/spec/smithy-json/codec_spec.rb new file mode 100644 index 000000000..b2e7dd1f6 --- /dev/null +++ b/gems/smithy-json/spec/smithy-json/codec_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require_relative '../spec_helper' + +module Smithy + module Json + describe Codec do + let(:shapes) { SchemaHelper.sample_shapes } + let(:sample_schema) { SchemaHelper.sample_schema(shapes: shapes) } + let(:structure_shape) { sample_schema.const_get(:Structure) } + + it 'reuses the same codec instance across build calls without leaking builder state' do + codec = described_class.new + + first = codec.build(structure_shape, { string: 'first' }) + second = codec.build(structure_shape, { integer: 123 }) + + expect(Smithy::Json.load(first)).to eq('string' => 'first') + expect(Smithy::Json.load(second)).to eq('integer' => 123) + end + + it 'reuses the same codec instance across parse calls' do + codec = described_class.new + + first = codec.parse(structure_shape, '{"string":"first"}') + second = codec.parse(structure_shape, '{"integer":123}') + + expect(first.to_h).to eq(string: 'first') + expect(second.to_h).to eq(integer: 123) + end + end + end +end diff --git a/gems/smithy-xml/lib/smithy-xml/builder.rb b/gems/smithy-xml/lib/smithy-xml/builder.rb index bca3e7b72..7ce437c2e 100644 --- a/gems/smithy-xml/lib/smithy-xml/builder.rb +++ b/gems/smithy-xml/lib/smithy-xml/builder.rb @@ -40,29 +40,32 @@ def blob(value) def list(name, shape, values) member_shape, = Schema::Extension.list_member(shape.target) - if Extension.flattened?(shape) + flattened = Extension.flattened?(shape) + if flattened values.each do |value| build_shape(name, member_shape, value) end else + member_name = Extension.wire_name(member_shape) node(name, shape) do values.each do |value| - build_shape(Extension.wire_name(member_shape), member_shape, value) + build_shape(member_name, member_shape, value) end end end end def map(name, shape, values) - if Extension.flattened?(shape) + flattened = Extension.flattened?(shape) + if flattened flat_map_entries(name, shape, values) else - key_name, key_shape, value_name, value_shape = Extension.map_parts(shape) + key_name, key_member, value_name, value_member = Extension.map_parts(shape) node(name, shape) do values.each do |key, value| node('entry', @map_entry_shape) do - build_shape(key_name, key_shape, key) - build_shape(value_name, value_shape, value) + build_shape(key_name, key_member, key) + build_shape(value_name, value_member, value) end end end @@ -70,11 +73,11 @@ def map(name, shape, values) end def flat_map_entries(name, shape, values) - key_name, key_shape, value_name, value_shape = Extension.map_parts(shape) + key_name, key_member, value_name, value_member = Extension.map_parts(shape) values.each do |key, value| node(name, shape) do - build_shape(key_name, key_shape, key) - build_shape(value_name, value_shape, value) + build_shape(key_name, key_member, key) + build_shape(value_name, value_member, value) end end end @@ -83,20 +86,23 @@ def structure(name, shape, values) return node(name, shape) if values.empty? node(name, shape, structure_attrs(shape, values)) do - Extension.element_members(shape.target).each do |ruby_member_name, xml_name, member_shape| - next if values[ruby_member_name].nil? + element_members = Extension.element_members(shape.target) + element_members.each do |member_name, xml_name, member_shape| + member_value = values[member_name] + next if member_value.nil? - build_shape(xml_name, member_shape, values[ruby_member_name]) + build_shape(xml_name, member_shape, member_value) end end end def structure_attrs(shape, values) - members = Extension.attribute_members(shape.target) - members.each_with_object({}) do |(ruby_member_name, xml_name, _member_shape), attrs| - next unless values.key?(ruby_member_name) + attribute_members = Extension.attribute_members(shape.target) + attribute_members.each_with_object({}) do |(name, xml_name, _member_shape), attrs| + value = values[name] + next if value.nil? && !values.key?(name) - attrs[xml_name] = values[ruby_member_name] + attrs[xml_name] = value end end @@ -111,21 +117,18 @@ def timestamp(shape, value) end end - def union(name, shape, values) # rubocop:disable Metrics/AbcSize + def union(name, shape, values) return node(name, shape) if values.empty? + if values.is_a?(Schema::Union) + key = values.member + value = values.value + else + key, value = values.first + end node(name, shape, structure_attrs(shape, values)) do - if values.is_a?(Schema::Union) - member_name, _member_shape = shape.target.member_by_type(values.class) - member_shape = shape.target.member(member_name) - build_shape(Extension.wire_name(member_shape), member_shape, values.value) - else - key, value = values.first - if shape.target.member?(key) - member_shape = shape.target.member(key) - build_shape(Extension.wire_name(member_shape), member_shape, value) - end - end + member_shape = shape.target.member(key) + build_shape(Extension.wire_name(member_shape), member_shape, value) if member_shape end end @@ -142,7 +145,9 @@ def union(name, shape, values) # rubocop:disable Metrics/AbcSize def node(name, shape, *args, &) attrs = args.last.is_a?(Hash) ? args.pop : {} namespace_attrs = Extension.namespace_attrs(shape) - attrs = attrs.empty? ? namespace_attrs : namespace_attrs.merge(attrs) if namespace_attrs + if namespace_attrs + attrs = attrs.empty? ? namespace_attrs : namespace_attrs.merge(attrs) + end args << attrs @builder.node(name, *args, &) end diff --git a/gems/smithy-xml/lib/smithy-xml/codec.rb b/gems/smithy-xml/lib/smithy-xml/codec.rb index 23ae8d86e..9d67125e6 100644 --- a/gems/smithy-xml/lib/smithy-xml/codec.rb +++ b/gems/smithy-xml/lib/smithy-xml/codec.rb @@ -6,7 +6,8 @@ module Xml class Codec # @param [Hash] options def initialize(options = {}) - @options = options + @builder = Builder.new(options) + @parser = Parser.new(options) end # @param [Shape] shape @@ -14,7 +15,7 @@ def initialize(options = {}) # @param [Array, nil] output (nil) # @return [String, nil] def build(shape, data, output = nil) - Builder.new(@options).build(shape, data, output) + @builder.build(shape, data, output) end # @param [Shape] shape @@ -22,7 +23,7 @@ def build(shape, data, output = nil) # @param [Object, nil] result (nil) # @return [Object, nil] def parse(shape, bytes, result = nil) - Parser.new(@options).parse(shape, bytes, result) + @parser.parse(shape, bytes, result) end end end diff --git a/gems/smithy-xml/lib/smithy-xml/parser/frame.rb b/gems/smithy-xml/lib/smithy-xml/parser/frame.rb index 9d0877da4..1a390a75a 100644 --- a/gems/smithy-xml/lib/smithy-xml/parser/frame.rb +++ b/gems/smithy-xml/lib/smithy-xml/parser/frame.rb @@ -94,7 +94,7 @@ def result class FlatListFrame < Frame def initialize(xml_name, *args) super - @member, _target_shape, _sparse = Schema::Extension.list_member(@shape.target) + @member, = Schema::Extension.list_member(@shape.target) @member = Frame.new(xml_name, self, @member) end @@ -139,7 +139,7 @@ class ListFrame < Frame def initialize(*args) super @result = [] - @member, _target_shape, _sparse = Schema::Extension.list_member(@shape.target) + @member, = Schema::Extension.list_member(@shape.target) @member_xml_name = Smithy::Xml::Extension.wire_name(@member) end @@ -160,10 +160,10 @@ def consume_child_frame(child) class MapEntryFrame < Frame def initialize(xml_name, *args) super - @key, _key_target_shape = Schema::Extension.map_key_member(@shape.target) + @key, = Schema::Extension.map_key_member(@shape.target) @key_name = Smithy::Xml::Extension.wire_name(@key) @key = Frame.new(xml_name, self, @key) - @value, _value_target_shape, _sparse = Schema::Extension.map_value_member(@shape.target) + @value, = Schema::Extension.map_value_member(@shape.target) @value_name = Smithy::Xml::Extension.wire_name(@value) @value = Frame.new(xml_name, self, @value) end diff --git a/gems/smithy-xml/spec/smithy-xml/builder_spec.rb b/gems/smithy-xml/spec/smithy-xml/builder_spec.rb index e1f696655..481f47032 100644 --- a/gems/smithy-xml/spec/smithy-xml/builder_spec.rb +++ b/gems/smithy-xml/spec/smithy-xml/builder_spec.rb @@ -215,6 +215,30 @@ def inline(xml) expect(bytes).to include('string') end + it 'builds flattened lists without a wrapper element' do + list = Schema::Shapes::ListShape.new + list.add_member( + :member, + Schema::Shapes::MemberShape.new( + target: Schema::Shapes::StringShape.new, + name: 'member' + ) + ) + shape = Schema::Shapes::StructureShape.new(name: 'Root') + shape.add_member( + :items, + Schema::Shapes::MemberShape.new( + target: list, + name: 'items', + traits: { 'smithy.api#xmlFlattened' => {} } + ) + ) + + expect(subject.build(shape, items: %w[one two])).to eq( + 'onetwo' + ) + end + it 'builds lists with nil values' do data = { list: [nil] } bytes = subject.build(structure_shape, data) @@ -229,6 +253,38 @@ def inline(xml) expect(bytes).to include('keyvalue') end + it 'builds flattened maps without an entry wrapper' do + map = Schema::Shapes::MapShape.new + map.add_member( + :key, + Schema::Shapes::MemberShape.new( + target: Schema::Shapes::StringShape.new, + name: 'key' + ) + ) + map.add_member( + :value, + Schema::Shapes::MemberShape.new( + target: Schema::Shapes::StringShape.new, + name: 'value' + ) + ) + shape = Schema::Shapes::StructureShape.new(name: 'Root') + shape.add_member( + :entries, + Schema::Shapes::MemberShape.new( + target: map, + name: 'entries', + traits: { 'smithy.api#xmlFlattened' => {} } + ) + ) + + expect(subject.build(shape, entries: { 'one' => 'first', 'two' => 'second' })).to eq( + 'onefirst' \ + 'twosecond' + ) + end + it 'builds maps with nil values' do data = { map: { 'key' => nil } } bytes = subject.build(structure_shape, data) diff --git a/gems/smithy-xml/spec/smithy-xml/codec_spec.rb b/gems/smithy-xml/spec/smithy-xml/codec_spec.rb new file mode 100644 index 000000000..651417e6c --- /dev/null +++ b/gems/smithy-xml/spec/smithy-xml/codec_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require_relative '../spec_helper' + +module Smithy + module Xml + describe Codec do + let(:shapes) { SchemaHelper.sample_shapes } + let(:sample_schema) { SchemaHelper.sample_schema(shapes: shapes) } + let(:structure_shape) { sample_schema.const_get(:Structure) } + + it 'reuses the same codec instance across build calls without leaking builder state' do + codec = described_class.new + + first = codec.build(structure_shape, { string: 'first' }) + second = codec.build(structure_shape, { integer: 123 }) + + expect(first).to eq('first') + expect(second).to eq('123') + end + + it 'reuses the same codec instance across parse calls' do + codec = described_class.new + + first = codec.parse(structure_shape, 'first') + second = codec.parse(structure_shape, '123') + + expect(first.to_h).to eq(string: 'first') + expect(second.to_h).to eq(integer: 123) + end + end + end +end From 431b1f11d29c60eb678046a0c0874fc8df418db0 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Tue, 15 Sep 2026 09:18:49 -0700 Subject: [PATCH 02/36] perf: optimize schema extension cache access --- .../lib/smithy-client/http_extension.rb | 39 +++++----- .../spec/smithy-client/http_extension_spec.rb | 8 ++ gems/smithy-json/lib/smithy-json/extension.rb | 30 +++++--- .../spec/smithy-json/extension_spec.rb | 11 +++ .../lib/smithy-schema/extension.rb | 76 ++++++++++--------- gems/smithy-xml/lib/smithy-xml/extension.rb | 33 ++++---- .../spec/smithy-xml/extension_spec.rb | 10 +++ 7 files changed, 125 insertions(+), 82 deletions(-) diff --git a/gems/smithy-client/lib/smithy-client/http_extension.rb b/gems/smithy-client/lib/smithy-client/http_extension.rb index ec28b2d12..9229c08d9 100644 --- a/gems/smithy-client/lib/smithy-client/http_extension.rb +++ b/gems/smithy-client/lib/smithy-client/http_extension.rb @@ -33,16 +33,7 @@ module HttpExtension class << self def fetch(shape) - return shape[KEY] if shape.key?(KEY) - - shape[KEY] = - if shape.is_a?(Schema::Shapes::OperationShape) - operation_metadata(shape) - elsif shape.respond_to?(:members) - shape_metadata(shape) - else - EMPTY_HASH - end + shape[KEY] || build_and_cache(shape) end # Returns header bindings as: @@ -52,7 +43,7 @@ def fetch(shape) # HttpExtension.header_members(shape) # # => [[:request_id, member, 'X-Request-Id']] def header_members(shape) - fetch(shape).fetch(:header_members, EMPTY_ARRAY) + (shape[KEY] || build_and_cache(shape)).fetch(:header_members, EMPTY_ARRAY) end # Returns the prefix-header binding as: @@ -62,7 +53,7 @@ def header_members(shape) # HttpExtension.prefix_header_member(shape) # # => [:metadata, member, 'x-amz-meta-'] def prefix_header_member(shape) - fetch(shape)[:prefix_header_member] + (shape[KEY] || build_and_cache(shape))[:prefix_header_member] end # Returns query bindings as: @@ -72,7 +63,7 @@ def prefix_header_member(shape) # HttpExtension.query_members(shape) # # => [[:page_size, member, 'pageSize']] def query_members(shape) - fetch(shape).fetch(:query_members, EMPTY_ARRAY) + (shape[KEY] || build_and_cache(shape)).fetch(:query_members, EMPTY_ARRAY) end # Returns the query-params binding as: @@ -82,7 +73,7 @@ def query_members(shape) # HttpExtension.query_params_member(shape) # # => [:filters, member] def query_params_member(shape) - fetch(shape)[:query_params_member] + (shape[KEY] || build_and_cache(shape))[:query_params_member] end # Returns labels indexed by modeled member name. @@ -91,7 +82,7 @@ def query_params_member(shape) # HttpExtension.label_index(shape) # # => { 'bucket' => [:bucket, member] } def label_index(shape) - fetch(shape).fetch(:label_index, EMPTY_HASH) + (shape[KEY] || build_and_cache(shape)).fetch(:label_index, EMPTY_HASH) end # Returns members serialized in the document body. @@ -100,7 +91,7 @@ def label_index(shape) # HttpExtension.body_members(shape) # # => [[:name, member]] def body_members(shape) - fetch(shape).fetch(:body_members, EMPTY_ARRAY) + (shape[KEY] || build_and_cache(shape)).fetch(:body_members, EMPTY_ARRAY) end # Returns the payload binding as: @@ -110,7 +101,7 @@ def body_members(shape) # HttpExtension.payload_member(shape) # # => [:body, member, :raw, 'application/octet-stream'] def payload_member(shape) - fetch(shape)[:payload_member] + (shape[KEY] || build_and_cache(shape))[:payload_member] end # Returns the response-code binding as: @@ -120,11 +111,23 @@ def payload_member(shape) # HttpExtension.response_code_member(shape) # # => [:status_code, member] def response_code_member(shape) - fetch(shape)[:response_code_member] + (shape[KEY] || build_and_cache(shape))[:response_code_member] end private + def build_and_cache(shape) + Schema::Extension.fetch(shape) + shape[KEY] = + if shape.is_a?(Schema::Shapes::OperationShape) + operation_metadata(shape) + elsif shape.respond_to?(:members) + shape_metadata(shape) + else + EMPTY_HASH + end + end + def operation_metadata(operation) http = operation.traits['smithy.api#http'] || {} path, static_query = (http['uri'] || '/').split('?', 2) diff --git a/gems/smithy-client/spec/smithy-client/http_extension_spec.rb b/gems/smithy-client/spec/smithy-client/http_extension_spec.rb index 6f21332eb..6655e3537 100644 --- a/gems/smithy-client/spec/smithy-client/http_extension_spec.rb +++ b/gems/smithy-client/spec/smithy-client/http_extension_spec.rb @@ -5,6 +5,14 @@ module Smithy module Client describe HttpExtension do + it 'populates shared schema metadata before empty HTTP metadata' do + shape = Schema::Shapes::StringShape.new + + expect(shape[:schema]).to be_nil + expect(described_class.fetch(shape)).to eq({}) + expect(shape[:schema]).to eq(target_shape: Schema::Extension::SHAPE_STRING) + end + it 'caches HTTP operation metadata' do operation = Schema::Shapes::OperationShape.new( traits: { 'smithy.api#http' => { 'method' => 'GET', 'uri' => '/things?x=1', 'code' => 204 } } diff --git a/gems/smithy-json/lib/smithy-json/extension.rb b/gems/smithy-json/lib/smithy-json/extension.rb index 726985ea3..0c608464b 100644 --- a/gems/smithy-json/lib/smithy-json/extension.rb +++ b/gems/smithy-json/lib/smithy-json/extension.rb @@ -11,6 +11,7 @@ module Json # @api private module Extension KEY = :json + EMPTY_METADATA = {}.freeze class << self # Resolves and returns JSON metadata for a structure, union, or member. @@ -19,15 +20,7 @@ class << self # Extension.fetch(member) # # => { json_name: 'wireName' } def fetch(shape) - return shape[KEY] if shape.key?(KEY) - - shape[KEY] = - case shape - when Schema::Shapes::StructureShape, Schema::Shapes::UnionShape - build_structure_metadata(shape) - when Schema::Shapes::MemberShape - build_member_metadata(shape) - end + shape[KEY] || build_and_cache(shape) end # Returns the JSON parse lookup index cached in structure or union @@ -41,7 +34,7 @@ def fetch(shape) # Extension.wire_index(shape) # # => { 'wireName' => [:ruby_name, member, Schema::Extension::SHAPE_STRING] } def wire_index(shape) - fetch(shape)[:json_wire_index] + (shape[KEY] || build_and_cache(shape))[:json_wire_index] end # Returns the JSON build lookup index cached in structure or union @@ -55,7 +48,7 @@ def wire_index(shape) # Extension.member_index(shape) # # => { ruby_name: ['wireName', member, Schema::Extension::SHAPE_STRING] } def member_index(shape) - fetch(shape)[:json_member_index] + (shape[KEY] || build_and_cache(shape))[:json_member_index] end # Returns the effective JSON member name: +smithy.api#jsonName+ when @@ -65,7 +58,7 @@ def member_index(shape) # Extension.wire_name(member) # # => 'wireName' def wire_name(member) - fetch(member)[:json_name] + (member[KEY] || build_and_cache(member))[:json_name] end # Returns the resolved timestamp format for JSON serialization. @@ -79,6 +72,19 @@ def timestamp_format(shape) private + def build_and_cache(shape) + Schema::Extension.fetch(shape) + shape[KEY] = + case shape + when Schema::Shapes::StructureShape, Schema::Shapes::UnionShape + build_structure_metadata(shape) + when Schema::Shapes::MemberShape + build_member_metadata(shape) + else + EMPTY_METADATA + end + end + def build_structure_metadata(shape) json_wire_index = {} json_member_index = {} diff --git a/gems/smithy-json/spec/smithy-json/extension_spec.rb b/gems/smithy-json/spec/smithy-json/extension_spec.rb index a3cebd727..cf8518b9c 100644 --- a/gems/smithy-json/spec/smithy-json/extension_spec.rb +++ b/gems/smithy-json/spec/smithy-json/extension_spec.rb @@ -60,6 +60,17 @@ module Json expect(plain_member[:json][:json_name]).to eq('plainName') end end + + describe '.fetch' do + it 'caches a truthy empty payload for unsupported shape kinds' do + shape = Schema::Shapes::StringShape.new + + expect(shape[:schema]).to be_nil + expect(described_class.fetch(shape)).to be_empty + expect(described_class.fetch(shape)).to be(shape[:json]) + expect(shape[:schema]).to eq(target_shape: Schema::Extension::SHAPE_STRING) + end + end end end end diff --git a/gems/smithy-schema/lib/smithy-schema/extension.rb b/gems/smithy-schema/lib/smithy-schema/extension.rb index 83300646c..823097752 100644 --- a/gems/smithy-schema/lib/smithy-schema/extension.rb +++ b/gems/smithy-schema/lib/smithy-schema/extension.rb @@ -51,19 +51,7 @@ class << self # 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 + shape[KEY] || build_and_cache(shape) end # Returns the modeled wire-name lookup used by existing serde @@ -74,7 +62,7 @@ def fetch(shape) # Extension.wire_index(shape) # # => { 'wireName' => [:ruby_name, member, Extension::SHAPE_STRING] } def wire_index(shape) - fetch(shape)[:wire_index] + (shape[KEY] || build_and_cache(shape))[:wire_index] end # Returns the canonical build lookup index. The index maps Ruby member @@ -84,7 +72,7 @@ def wire_index(shape) # Extension.member_index(shape) # # => { ruby_name: ['wireName', member, Extension::SHAPE_STRING] } def member_index(shape) - fetch(shape)[:member_index] + (shape[KEY] || build_and_cache(shape))[:member_index] end # Returns a normalized reference for the target shape of +shape+. @@ -95,7 +83,7 @@ def member_index(shape) # Extension.target_shape(member) # # => Extension::SHAPE_STRING def target_shape(shape) - fetch(shape)[:target_shape] + (shape[KEY] || build_and_cache(shape))[:target_shape] end # Returns [member_shape, target_shape_ref, sparse] for a list. @@ -104,7 +92,7 @@ def target_shape(shape) # Extension.list_member(list) # # => [member, Extension::SHAPE_STRING, true] def list_member(shape) - fetch(shape)[:list_member] + (shape[KEY] || build_and_cache(shape))[:list_member] end # Returns [member_shape, target_shape_ref] for a map key. @@ -113,7 +101,7 @@ def list_member(shape) # Extension.map_key_member(map) # # => [member, Extension::SHAPE_STRING] def map_key_member(shape) - fetch(shape)[:map_key_member] + (shape[KEY] || build_and_cache(shape))[:map_key_member] end # Returns [member_shape, target_shape_ref, sparse] for a map value. @@ -122,7 +110,7 @@ def map_key_member(shape) # Extension.map_value_member(map) # # => [member, Extension::SHAPE_STRING, false] def map_value_member(shape) - fetch(shape)[:map_value_member] + (shape[KEY] || build_and_cache(shape))[:map_value_member] end # Returns the modeled media type, when present. @@ -131,42 +119,42 @@ def map_value_member(shape) # Extension.media_type(shape) # # => 'application/octet-stream' def media_type(shape) - fetch(shape)[:media_type] + (shape[KEY] || build_and_cache(shape))[:media_type] end # Returns whether the sensitive trait is present. def sensitive?(shape) - fetch(shape)[:sensitive] + (shape[KEY] || build_and_cache(shape))[:sensitive] end # Returns whether the streaming trait is present. def streaming?(shape) - fetch(shape)[:streaming] + (shape[KEY] || build_and_cache(shape))[:streaming] end # Returns whether the requires-length trait is present. def requires_length?(shape) - fetch(shape)[:requires_length] + (shape[KEY] || build_and_cache(shape))[:requires_length] end def endpoint_host_prefix(operation) - fetch(operation)[:endpoint_host_prefix] + (operation[KEY] || build_and_cache(operation))[:endpoint_host_prefix] end def request_compression_encodings(operation) - fetch(operation)[:request_compression_encodings] + (operation[KEY] || build_and_cache(operation))[:request_compression_encodings] end def checksum_required?(operation) - fetch(operation)[:checksum_required] + (operation[KEY] || build_and_cache(operation))[:checksum_required] end def long_polling?(operation) - fetch(operation)[:long_polling] + (operation[KEY] || build_and_cache(operation))[:long_polling] end def unsigned_payload?(operation) - fetch(operation)[:unsigned_payload] + (operation[KEY] || build_and_cache(operation))[:unsigned_payload] end # Returns operation errors indexed by target shape name. @@ -175,31 +163,31 @@ def unsigned_payload?(operation) # Extension.error_index(operation)['ResourceNotFound'] # # => error_member def error_index(operation) - fetch(operation).fetch(:error_index, {}.freeze) + (operation[KEY] || build_and_cache(operation)).fetch(:error_index, {}.freeze) end def required_members(shape) - fetch(shape).fetch(:required_members, [].freeze) + (shape[KEY] || build_and_cache(shape)).fetch(:required_members, [].freeze) end def host_label_index(shape) - fetch(shape).fetch(:host_label_index, {}.freeze) + (shape[KEY] || build_and_cache(shape)).fetch(:host_label_index, {}.freeze) end def idempotency_token_member(shape) - fetch(shape)[:idempotency_token_member] + (shape[KEY] || build_and_cache(shape))[:idempotency_token_member] end def streaming_member(shape) - fetch(shape)[:streaming_member] + (shape[KEY] || build_and_cache(shape))[:streaming_member] end def streaming_member_unknown_length(shape) - fetch(shape)[:streaming_member_unknown_length] + (shape[KEY] || build_and_cache(shape))[:streaming_member_unknown_length] end def event_stream_member(shape) - fetch(shape)[:event_stream_member] + (shape[KEY] || build_and_cache(shape))[:event_stream_member] end # Returns the effective timestamp format, or +:default+ when the @@ -209,7 +197,7 @@ def event_stream_member(shape) # Extension.timestamp_format(member) # # => 'date-time' def timestamp_format(shape) - fetch(shape).fetch(:timestamp_format, :default) + (shape[KEY] || build_and_cache(shape)).fetch(:timestamp_format, :default) end # Returns a modeled union's unknown-member type when present. @@ -218,7 +206,7 @@ def timestamp_format(shape) # Extension.unknown_member_type(union) # # => Types::Unknown def unknown_member_type(shape) - fetch(shape)[:unknown_member_type] + (shape[KEY] || build_and_cache(shape))[:unknown_member_type] end # Iterates modeled members with separate Ruby name and member-shape @@ -243,6 +231,20 @@ def sparse?(shape) private + def build_and_cache(shape) + 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 + def build_operation_metadata(operation) traits = operation.traits { diff --git a/gems/smithy-xml/lib/smithy-xml/extension.rb b/gems/smithy-xml/lib/smithy-xml/extension.rb index caee37a96..e2d43bd78 100644 --- a/gems/smithy-xml/lib/smithy-xml/extension.rb +++ b/gems/smithy-xml/lib/smithy-xml/extension.rb @@ -19,14 +19,7 @@ class << self # Extension.fetch(member) # # => { xml_wire_name: 'Item', ... } def fetch(shape) - return shape[KEY] if shape.key?(KEY) - - shape[KEY] = - if shape.is_a?(Schema::Shapes::MemberShape) - build_member_metadata(shape) - else - build_shape_metadata(shape) - end + shape[KEY] || build_and_cache(shape) end # Returns the XML wrapper or structure name. @@ -35,7 +28,7 @@ def fetch(shape) # Extension.structure_name(shape) # # => 'Result' def structure_name(shape) - fetch(shape)[:xml_structure_name] + (shape[KEY] || build_and_cache(shape))[:xml_structure_name] end # Preserves the existing true-or-nil return contract. @@ -53,7 +46,7 @@ def flattened?(shape) # Extension.frame_class(shape) # # => Parser::ListFrame def frame_class(shape) - fetch(shape)[:xml_frame_class] + (shape[KEY] || build_and_cache(shape))[:xml_frame_class] end # Returns the resolved XML member name. @@ -62,7 +55,7 @@ def frame_class(shape) # Extension.wire_name(member) # # => 'Item' def wire_name(member) - fetch(member)[:xml_wire_name] + (member[KEY] || build_and_cache(member))[:xml_wire_name] end # Returns XML members partitioned into attributes and elements. @@ -71,7 +64,7 @@ def wire_name(member) # Extension.members(shape) # # => { attributes: [...], elements: [...] } def members(shape) - fetch(shape)[:xml_members] + (shape[KEY] || build_and_cache(shape))[:xml_members] end def attribute_members(shape) @@ -83,15 +76,15 @@ def element_members(shape) end def member_index(shape) - fetch(shape)[:xml_member_index] + (shape[KEY] || build_and_cache(shape))[:xml_member_index] end def namespace_attrs(shape) - fetch(shape)[:xml_namespace_attrs] + (shape[KEY] || build_and_cache(shape))[:xml_namespace_attrs] end def map_parts(shape) - fetch(shape)[:xml_map_parts] + (shape[KEY] || build_and_cache(shape))[:xml_map_parts] end def timestamp_format(shape) @@ -104,6 +97,16 @@ def sparse?(shape) private + def build_and_cache(shape) + Schema::Extension.fetch(shape) + shape[KEY] = + if shape.is_a?(Schema::Shapes::MemberShape) + build_member_metadata(shape) + else + build_shape_metadata(shape) + end + end + def build_shape_metadata(shape) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength target = shape.target target_shape = Schema::Extension.target_shape(shape) diff --git a/gems/smithy-xml/spec/smithy-xml/extension_spec.rb b/gems/smithy-xml/spec/smithy-xml/extension_spec.rb index 3e5742c3c..6087f42fd 100644 --- a/gems/smithy-xml/spec/smithy-xml/extension_spec.rb +++ b/gems/smithy-xml/spec/smithy-xml/extension_spec.rb @@ -41,6 +41,16 @@ module Xml end end + describe '.fetch' do + it 'populates shared schema metadata before XML metadata' do + shape = Schema::Shapes::StringShape.new + + expect(shape[:schema]).to be_nil + described_class.fetch(shape) + expect(shape[:schema]).to eq(target_shape: Schema::Extension::SHAPE_STRING) + end + end + describe '.wire_name' do it 'prefers xmlName when present' do member = Schema::Shapes::MemberShape.new( From 9d23058c23724ac6a1ae5d78d5de1530b24507c6 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Tue, 15 Sep 2026 13:07:56 -0700 Subject: [PATCH 03/36] perf: reference collection shapes directly --- gems/smithy-cbor/lib/smithy-cbor/builder.rb | 17 ++--- gems/smithy-cbor/lib/smithy-cbor/parser.rb | 17 ++--- .../lib/smithy-client/default_params.rb | 4 +- .../lib/smithy-client/log_param_filter.rb | 4 +- .../lib/smithy-client/param_converter.rb | 6 +- .../lib/smithy-client/param_validator.rb | 6 +- gems/smithy-json/lib/smithy-json/builder.rb | 20 ++---- gems/smithy-json/lib/smithy-json/extension.rb | 9 ++- gems/smithy-json/lib/smithy-json/parser.rb | 29 ++++---- .../smithy-json/sig/smithy-json/extension.rbs | 4 +- .../spec/smithy-json/extension_spec.rb | 8 +-- .../document_utils/deserializer.rb | 11 ++- .../lib/smithy-schema/extension.rb | 68 ++----------------- .../sig/smithy-schema/extension.rbs | 8 +-- .../spec/smithy-schema/extension_spec.rb | 19 +----- gems/smithy-xml/lib/smithy-xml/builder.rb | 2 +- gems/smithy-xml/lib/smithy-xml/extension.rb | 4 +- .../smithy-xml/lib/smithy-xml/parser/frame.rb | 8 +-- 18 files changed, 77 insertions(+), 167 deletions(-) diff --git a/gems/smithy-cbor/lib/smithy-cbor/builder.rb b/gems/smithy-cbor/lib/smithy-cbor/builder.rb index f43928891..ffda40d4f 100644 --- a/gems/smithy-cbor/lib/smithy-cbor/builder.rb +++ b/gems/smithy-cbor/lib/smithy-cbor/builder.rb @@ -34,7 +34,7 @@ def blob(value) def list(shape, values) return if values.nil? - member, _target_shape, _sparse = Schema::Extension.list_member(shape.target) + member = shape.target.member values.collect do |value| build_shape(member, value) end @@ -43,7 +43,7 @@ def list(shape, values) def map(shape, values) return if values.nil? - value_member, _target_shape, _sparse = Schema::Extension.map_value_member(shape.target) + value_member = shape.target.value values.each.with_object({}) do |(key, value), data| data[key] = build_shape(value_member, value) end @@ -52,13 +52,11 @@ def map(shape, values) def structure(shape, values) return if values.nil? - index = Schema::Extension.member_index(shape.target) values.each_pair.with_object({}) do |(member_name, value), data| next if value.nil? - next unless (entry = index[member_name]) + next unless (member_shape = shape.target.member(member_name)) - wire_name, member_shape, _target_shape = entry - data[wire_name] = build_shape(member_shape, value) + data[member_shape.name] = build_shape(member_shape, value) end end @@ -71,11 +69,10 @@ def union(shape, values) else values.first end - entry = Schema::Extension.member_index(shape.target)[key] - return {} unless entry + member_shape = shape.target.member(key) + return {} unless member_shape - wire_name, member_shape, _target_shape = entry - { wire_name => build_shape(member_shape, value) } + { member_shape.name => build_shape(member_shape, value) } end end end diff --git a/gems/smithy-cbor/lib/smithy-cbor/parser.rb b/gems/smithy-cbor/lib/smithy-cbor/parser.rb index 9c70e1e0c..b9cc03e91 100644 --- a/gems/smithy-cbor/lib/smithy-cbor/parser.rb +++ b/gems/smithy-cbor/lib/smithy-cbor/parser.rb @@ -29,7 +29,9 @@ def parse_shape(shape, value, result = nil) end def list(shape, values, result = nil) - list_member, _target_shape, sparse = Schema::Extension.list_member(shape.target) + target = shape.target + list_member = target.member + sparse = target.traits.key?('smithy.api#sparse') result = [] if result.nil? values.each do |value| next if value.nil? && !sparse @@ -40,7 +42,9 @@ def list(shape, values, result = nil) end def map(shape, values, result = nil) - value_member, _target_shape, sparse = Schema::Extension.map_value_member(shape.target) + target = shape.target + value_member = target.value + sparse = target.traits.key?('smithy.api#sparse') result = {} if result.nil? values.each do |key, value| next if value.nil? && !sparse @@ -59,7 +63,7 @@ def structure(shape, values, result = nil) entry = index[wire_name] next unless entry - member_name, member_shape, _target_shape = entry + member_name, member_shape = entry result[member_name] = parse_shape(member_shape, value) end result @@ -73,17 +77,14 @@ def union(shape, values, result = nil) # rubocop:disable Metrics/AbcSize entry = index[wire_name] next unless entry - member_name, member_shape, _target_shape = entry + member_name, member_shape = entry result = shape.target.member_type(member_name) if result.nil? return result.new(member_name => parse_shape(member_shape, value)) end values.delete('__type') key, value = values.first - unknown_member_type = - Schema::Extension.unknown_member_type(shape.target) || - shape.target.member_type(:unknown) - unknown_member_type.new(unknown: { key => value }) + shape.target.member_type(:unknown).new(unknown: { key => value }) end end end diff --git a/gems/smithy-client/lib/smithy-client/default_params.rb b/gems/smithy-client/lib/smithy-client/default_params.rb index 00170731f..b19b076d1 100644 --- a/gems/smithy-client/lib/smithy-client/default_params.rb +++ b/gems/smithy-client/lib/smithy-client/default_params.rb @@ -31,7 +31,7 @@ def apply_shape(shape, value) def list(shape, values) return if values.nil? - member, = Schema::Extension.list_member(shape.target) + member = shape.target.member values.each do |value| apply_shape(member, value) end @@ -41,7 +41,7 @@ def list(shape, values) def map(shape, values) return if values.nil? - value_shape, = Schema::Extension.map_value_member(shape.target) + value_shape = shape.target.value values.each_pair do |_key, value| apply_shape(value_shape, value) end diff --git a/gems/smithy-client/lib/smithy-client/log_param_filter.rb b/gems/smithy-client/lib/smithy-client/log_param_filter.rb index 53a444ea8..5822b8143 100644 --- a/gems/smithy-client/lib/smithy-client/log_param_filter.rb +++ b/gems/smithy-client/lib/smithy-client/log_param_filter.rb @@ -24,7 +24,7 @@ def list(shape, values) target = shape.target return '[FILTERED]' if sensitive?(target) - member, = Schema::Extension.list_member(target) + member = target.member values.collect { |value| filter(member, value) } end @@ -33,7 +33,7 @@ def map(shape, values) return '[FILTERED]' if sensitive?(target) filtered = {} - value_shape, = Schema::Extension.map_value_member(target) + value_shape = target.value values.each_pair do |key, value| filtered[key] = filter(value_shape, value) end diff --git a/gems/smithy-client/lib/smithy-client/param_converter.rb b/gems/smithy-client/lib/smithy-client/param_converter.rb index 6cca6ac3d..326c32b0d 100644 --- a/gems/smithy-client/lib/smithy-client/param_converter.rb +++ b/gems/smithy-client/lib/smithy-client/param_converter.rb @@ -53,7 +53,7 @@ def list(shape, values) values = c(shape, values) return values unless values.is_a?(Array) - member, = Schema::Extension.list_member(shape.target) + member = shape.target.member values.collect { |v| convert_shape(member, v) } end @@ -61,8 +61,8 @@ def map(shape, values) values = c(shape, values) return values unless values.is_a?(Hash) - key_member, = Schema::Extension.map_key_member(shape.target) - value_member, = Schema::Extension.map_value_member(shape.target) + key_member = shape.target.key + value_member = shape.target.value values.each.with_object({}) do |(key, value), hash| hash[convert_shape(key_member, key)] = convert_shape(value_member, value) end diff --git a/gems/smithy-client/lib/smithy-client/param_validator.rb b/gems/smithy-client/lib/smithy-client/param_validator.rb index 1c1427e0e..dbf2a3fb9 100644 --- a/gems/smithy-client/lib/smithy-client/param_validator.rb +++ b/gems/smithy-client/lib/smithy-client/param_validator.rb @@ -91,7 +91,7 @@ def list(shape, values, errors, context) return end - member, = Schema::Extension.list_member(shape.target) + member = shape.target.member values.each.with_index do |value, index| next unless value @@ -105,8 +105,8 @@ def map(shape, values, errors, context) return end - key_member, = Schema::Extension.map_key_member(shape.target) - value_member, = Schema::Extension.map_value_member(shape.target) + key_member = shape.target.key + value_member = shape.target.value values.each do |key, value| validate_shape(key_member, key, errors, "#{context} #{key.inspect} key") next unless value diff --git a/gems/smithy-json/lib/smithy-json/builder.rb b/gems/smithy-json/lib/smithy-json/builder.rb index 21b902a87..40176675d 100644 --- a/gems/smithy-json/lib/smithy-json/builder.rb +++ b/gems/smithy-json/lib/smithy-json/builder.rb @@ -8,6 +8,7 @@ module Json class Builder def initialize(options = {}) @json_name = options[:json_name] || false + @extension = @json_name ? Extension : Schema::Extension @default_timestamp = options.fetch(:default_timestamp, 'epoch-seconds') end @@ -49,7 +50,7 @@ def float(value) def list(shape, values) return if values.nil? - member, _target_shape, _sparse = Schema::Extension.list_member(shape.target) + member = shape.target.member values.collect do |value| build_shape(member, value) end @@ -58,7 +59,7 @@ def list(shape, values) def map(shape, values) return if values.nil? - value_member, _target_shape, _sparse = Schema::Extension.map_value_member(shape.target) + value_member = shape.target.value values.each.with_object({}) do |(key, value), data| data[key] = build_shape(value_member, value) end @@ -67,12 +68,12 @@ def map(shape, values) def structure(shape, values) return if values.nil? - index = member_index(shape.target) + index = @extension.member_index(shape.target) values.each_pair.with_object({}) do |(member_name, value), data| next if value.nil? next unless (entry = index[member_name]) - wire_name, member_shape, _target_shape = entry + wire_name, member_shape = entry data[wire_name] = build_shape(member_shape, value) end end @@ -99,20 +100,13 @@ def union(shape, values) else values.first end - entry = member_index(shape.target)[key] + entry = @extension.member_index(shape.target)[key] return {} unless entry - wire_name, member_shape, _target_shape = entry + wire_name, member_shape = entry { wire_name => build_shape(member_shape, value) } end - def member_index(shape) - if @json_name - Extension.member_index(shape) - else - Schema::Extension.member_index(shape) - end - end end end end diff --git a/gems/smithy-json/lib/smithy-json/extension.rb b/gems/smithy-json/lib/smithy-json/extension.rb index 0c608464b..b81d00c1a 100644 --- a/gems/smithy-json/lib/smithy-json/extension.rb +++ b/gems/smithy-json/lib/smithy-json/extension.rb @@ -28,7 +28,7 @@ def fetch(shape) # # The index maps: # - resolved JSON wire name - # - to [ruby_member_name, member_shape, target_shape_ref] + # - to [ruby_member_name, member_shape] # # Example: # Extension.wire_index(shape) @@ -42,7 +42,7 @@ def wire_index(shape) # # The index maps: # - Ruby member name - # - to [resolved JSON wire name, member_shape, target_shape_ref] + # - to [resolved JSON wire name, member_shape] # # Example: # Extension.member_index(shape) @@ -91,9 +91,8 @@ def build_structure_metadata(shape) Schema::Extension.each_member(shape) do |member_name, member_shape| json_name = wire_name(member_shape) - target_shape = Schema::Extension.target_shape(member_shape) - json_wire_index[json_name] = [member_name, member_shape, target_shape].freeze - json_member_index[member_name] = [json_name, member_shape, target_shape].freeze + json_wire_index[json_name] = [member_name, member_shape].freeze + json_member_index[member_name] = [json_name, member_shape].freeze end { diff --git a/gems/smithy-json/lib/smithy-json/parser.rb b/gems/smithy-json/lib/smithy-json/parser.rb index 8bc6f8c6d..b94c3512b 100644 --- a/gems/smithy-json/lib/smithy-json/parser.rb +++ b/gems/smithy-json/lib/smithy-json/parser.rb @@ -8,6 +8,7 @@ module Json class Parser def initialize(options = {}) @json_name = options[:json_name] || false + @extension = @json_name ? Extension : Schema::Extension end def parse(shape, bytes, result = nil) @@ -43,7 +44,9 @@ def float(value) def list(shape, values, result = nil) return if values.nil? - member, _target_shape, sparse = Schema::Extension.list_member(shape.target) + target = shape.target + member = target.member + sparse = target.traits.key?('smithy.api#sparse') result = [] if result.nil? values.each do |value| next if value.nil? && !sparse @@ -54,7 +57,9 @@ def list(shape, values, result = nil) end def map(shape, values, result = nil) - value_member, _target_shape, sparse = Schema::Extension.map_value_member(shape.target) + target = shape.target + value_member = target.value + sparse = target.traits.key?('smithy.api#sparse') result = {} if result.nil? values.each do |key, value| next if value.nil? && !sparse @@ -68,14 +73,14 @@ def structure(shape, values, result = nil) return if values.nil? result = shape.target.type.new if result.nil? - index = wire_index(shape.target) + index = @extension.wire_index(shape.target) values.each do |wire_name, value| next if value.nil? entry = index[wire_name] next unless entry - member_name, member_shape, _target_shape = entry + member_name, member_shape = entry result[member_name] = parse_shape(member_shape, value) end result @@ -95,33 +100,23 @@ def timestamp(value) end def union(shape, values, result = nil) # rubocop:disable Metrics/AbcSize - index = wire_index(shape.target) + index = @extension.wire_index(shape.target) values.each do |wire_name, value| next if value.nil? entry = index[wire_name] next unless entry - member_name, member_shape, _target_shape = entry + member_name, member_shape = entry result = shape.target.member_type(member_name) if result.nil? return result.new(member_name => parse_shape(member_shape, value)) end values.delete('__type') key, value = values.first - unknown_member_type = - Schema::Extension.unknown_member_type(shape.target) || - shape.target.member_type(:unknown) - unknown_member_type.new(unknown: { key => value }) + shape.target.member_type(:unknown).new(unknown: { key => value }) end - def wire_index(shape) - if @json_name - Extension.wire_index(shape) - else - Schema::Extension.wire_index(shape) - end - end end end end diff --git a/gems/smithy-json/sig/smithy-json/extension.rbs b/gems/smithy-json/sig/smithy-json/extension.rbs index 7d7225836..322381107 100644 --- a/gems/smithy-json/sig/smithy-json/extension.rbs +++ b/gems/smithy-json/sig/smithy-json/extension.rbs @@ -5,8 +5,8 @@ module Smithy type serde_shape = aggregate_shape | Schema::Shapes::MemberShape def self.fetch: (serde_shape shape) -> Hash[Symbol, untyped] - def self.wire_index: (aggregate_shape shape) -> Hash[String?, [Symbol, Schema::Shapes::MemberShape, Integer?]] - def self.member_index: (aggregate_shape shape) -> Hash[Symbol, [String?, Schema::Shapes::MemberShape, Integer?]] + def self.wire_index: (aggregate_shape shape) -> Hash[String?, [Symbol, Schema::Shapes::MemberShape]] + def self.member_index: (aggregate_shape shape) -> Hash[Symbol, [String?, Schema::Shapes::MemberShape]] def self.timestamp_format: ((Schema::Shapes::Shape | Schema::Shapes::MemberShape) shape) -> (String | Symbol) def self.wire_name: (Schema::Shapes::MemberShape member) -> String? end diff --git a/gems/smithy-json/spec/smithy-json/extension_spec.rb b/gems/smithy-json/spec/smithy-json/extension_spec.rb index cf8518b9c..ccd78f320 100644 --- a/gems/smithy-json/spec/smithy-json/extension_spec.rb +++ b/gems/smithy-json/spec/smithy-json/extension_spec.rb @@ -26,8 +26,8 @@ module Json shape.add_member(:json_named, json_named_member) expect(described_class.wire_index(shape)).to eq( - 'plainName' => [:plain_name, plain_member, Schema::Extension::SHAPE_STRING], - 'wireName' => [:json_named, json_named_member, Schema::Extension::SHAPE_STRING] + 'plainName' => [:plain_name, plain_member], + 'wireName' => [:json_named, json_named_member] ) expect(described_class.wire_index(shape)).to be_frozen expect(plain_member[:json][:json_name]).to eq('plainName') @@ -42,8 +42,8 @@ module Json shape.add_member(:json_named, json_named_member) expect(described_class.member_index(shape)).to eq( - plain_name: ['plainName', plain_member, Schema::Extension::SHAPE_STRING], - json_named: ['wireName', json_named_member, Schema::Extension::SHAPE_STRING] + plain_name: ['plainName', plain_member], + json_named: ['wireName', json_named_member] ) expect(described_class.member_index(shape)).to be_frozen 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 dbc40c42d..f2890ab2d 100644 --- a/gems/smithy-schema/lib/smithy-schema/document_utils/deserializer.rb +++ b/gems/smithy-schema/lib/smithy-schema/document_utils/deserializer.rb @@ -75,9 +75,8 @@ def structure(shape, values, result = nil) return if values.nil? result = shape.target.type.new if result.nil? - Smithy::Schema::Extension.wire_index(shape.target).each do |wire_name, entry| - member_name, member_shape, _target_shape = entry - value = values[wire_name] + shape.target.members.each do |member_name, member_shape| + value = values[member_shape.name] result[member_name] = deserialize_shape(member_shape, value) unless value.nil? end result @@ -101,12 +100,10 @@ def timestamp(value) end def union(shape, values, result = nil) # rubocop:disable Metrics/AbcSize - index = Smithy::Schema::Extension.wire_index(shape.target) - values.each do |wire_name, value| + shape.target.members.each do |member_name, member_shape| + value = values[member_shape.name] next if value.nil? - next unless (entry = index[wire_name]) - member_name, member_shape, = entry result = shape.target.member_type(member_name) if result.nil? return result.new(member_name => deserialize_shape(member_shape, value)) end diff --git a/gems/smithy-schema/lib/smithy-schema/extension.rb b/gems/smithy-schema/lib/smithy-schema/extension.rb index 823097752..460779fff 100644 --- a/gems/smithy-schema/lib/smithy-schema/extension.rb +++ b/gems/smithy-schema/lib/smithy-schema/extension.rb @@ -56,21 +56,21 @@ def fetch(shape) # 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]. + # [ruby_member_name, member_shape]. # # Example: # Extension.wire_index(shape) - # # => { 'wireName' => [:ruby_name, member, Extension::SHAPE_STRING] } + # # => { 'wireName' => [:ruby_name, member] } def wire_index(shape) (shape[KEY] || build_and_cache(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]. + # name to [modeled_member_name, member_shape]. # # Example: # Extension.member_index(shape) - # # => { ruby_name: ['wireName', member, Extension::SHAPE_STRING] } + # # => { ruby_name: ['wireName', member] } def member_index(shape) (shape[KEY] || build_and_cache(shape))[:member_index] end @@ -86,33 +86,6 @@ def target_shape(shape) (shape[KEY] || build_and_cache(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) - (shape[KEY] || build_and_cache(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) - (shape[KEY] || build_and_cache(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) - (shape[KEY] || build_and_cache(shape))[:map_value_member] - end - # Returns the modeled media type, when present. # # Example: @@ -200,15 +173,6 @@ def timestamp_format(shape) (shape[KEY] || build_and_cache(shape)).fetch(:timestamp_format, :default) end - # Returns a modeled union's unknown-member type when present. - # - # Example: - # Extension.unknown_member_type(union) - # # => Types::Unknown - def unknown_member_type(shape) - (shape[KEY] || build_and_cache(shape))[:unknown_member_type] - end - # Iterates modeled members with separate Ruby name and member-shape # arguments. With no block, returns the underlying enumerator. # @@ -267,12 +231,9 @@ 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 @@ -297,9 +258,8 @@ def build_aggregate_metadata(shape) 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 + wire_index[modeled_name] = [ruby_name, member].freeze + member_index[ruby_name] = [modeled_name, member].freeze if member.traits.key?('smithy.api#required') && !member.traits.key?('smithy.api#clientOptional') required_members << ruby_name @@ -309,7 +269,7 @@ def build_aggregate_metadata(shape) next unless streaming_trait?(member.target) metadata[:streaming_member] ||= member - metadata[:event_stream_member] ||= member if target_shape == SHAPE_UNION + metadata[:event_stream_member] ||= member if member.target.class == Shapes::UnionShape metadata[:streaming_member_unknown_length] ||= member unless requires_length_trait?(member.target) end @@ -320,16 +280,6 @@ def build_aggregate_metadata(shape) 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 @@ -358,10 +308,6 @@ 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 end diff --git a/gems/smithy-schema/sig/smithy-schema/extension.rbs b/gems/smithy-schema/sig/smithy-schema/extension.rbs index 65d7899a4..5a3652c97 100644 --- a/gems/smithy-schema/sig/smithy-schema/extension.rbs +++ b/gems/smithy-schema/sig/smithy-schema/extension.rbs @@ -2,12 +2,9 @@ module Smithy module Schema module Extension 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.wire_index: ((Shapes::StructureShape | Shapes::UnionShape) shape) -> Hash[String, [Symbol, Shapes::MemberShape]] + def self.member_index: ((Shapes::StructureShape | Shapes::UnionShape) shape) -> Hash[Symbol, [String, Shapes::MemberShape]] 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? @@ -24,7 +21,6 @@ module Smithy 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.sparse?: ((Shapes::Shape | Shapes::MemberShape) shape) -> bool end diff --git a/gems/smithy-schema/spec/smithy-schema/extension_spec.rb b/gems/smithy-schema/spec/smithy-schema/extension_spec.rb index d620a4fee..9daa25ebf 100644 --- a/gems/smithy-schema/spec/smithy-schema/extension_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/extension_spec.rb @@ -12,7 +12,7 @@ module Schema it 'returns a frozen member index keyed by member name' do shape.add_member(:some_member, member) - expected_values = [:some_member, member, described_class::SHAPE_STRING] + expected_values = [:some_member, member] expect(described_class.wire_index(shape)).to eq('wireName' => expected_values) expect(described_class.wire_index(shape)).to be_frozen end @@ -36,7 +36,7 @@ module Schema shape = Shapes::StructureShape.new shape.add_member(:some_member, member) - expected_values = ['wireName', member, described_class::SHAPE_STRING] + expected_values = ['wireName', member] expect(described_class.member_index(shape)).to eq(some_member: expected_values) end end @@ -65,14 +65,6 @@ module Schema expect(described_class.target_shape(Shapes::ListShape.new)).to eq(described_class::SHAPE_LIST) end - 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' } @@ -94,13 +86,6 @@ module Schema 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 end diff --git a/gems/smithy-xml/lib/smithy-xml/builder.rb b/gems/smithy-xml/lib/smithy-xml/builder.rb index 7ce437c2e..fec30239d 100644 --- a/gems/smithy-xml/lib/smithy-xml/builder.rb +++ b/gems/smithy-xml/lib/smithy-xml/builder.rb @@ -39,7 +39,7 @@ def blob(value) end def list(name, shape, values) - member_shape, = Schema::Extension.list_member(shape.target) + member_shape = shape.target.member flattened = Extension.flattened?(shape) if flattened values.each do |value| diff --git a/gems/smithy-xml/lib/smithy-xml/extension.rb b/gems/smithy-xml/lib/smithy-xml/extension.rb index e2d43bd78..1ea8e6d04 100644 --- a/gems/smithy-xml/lib/smithy-xml/extension.rb +++ b/gems/smithy-xml/lib/smithy-xml/extension.rb @@ -159,8 +159,8 @@ def build_member_metadata(member) # rubocop:disable Metrics/AbcSize def add_map_parts(metadata, target) return unless Schema::Extension.target_shape(target) == Schema::Extension::SHAPE_MAP - key_member, = Schema::Extension.map_key_member(target) - value_member, = Schema::Extension.map_value_member(target) + key_member = target.key + value_member = target.value return unless key_member && value_member metadata[:xml_map_parts] = [ diff --git a/gems/smithy-xml/lib/smithy-xml/parser/frame.rb b/gems/smithy-xml/lib/smithy-xml/parser/frame.rb index 1a390a75a..d664cb42a 100644 --- a/gems/smithy-xml/lib/smithy-xml/parser/frame.rb +++ b/gems/smithy-xml/lib/smithy-xml/parser/frame.rb @@ -94,7 +94,7 @@ def result class FlatListFrame < Frame def initialize(xml_name, *args) super - @member, = Schema::Extension.list_member(@shape.target) + @member = @shape.target.member @member = Frame.new(xml_name, self, @member) end @@ -139,7 +139,7 @@ class ListFrame < Frame def initialize(*args) super @result = [] - @member, = Schema::Extension.list_member(@shape.target) + @member = @shape.target.member @member_xml_name = Smithy::Xml::Extension.wire_name(@member) end @@ -160,10 +160,10 @@ def consume_child_frame(child) class MapEntryFrame < Frame def initialize(xml_name, *args) super - @key, = Schema::Extension.map_key_member(@shape.target) + @key = @shape.target.key @key_name = Smithy::Xml::Extension.wire_name(@key) @key = Frame.new(xml_name, self, @key) - @value, = Schema::Extension.map_value_member(@shape.target) + @value = @shape.target.value @value_name = Smithy::Xml::Extension.wire_name(@value) @value = Frame.new(xml_name, self, @value) end From 91f7cc627292494e2f444ef9379c369a1829bff9 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Wed, 16 Sep 2026 13:16:23 -0700 Subject: [PATCH 04/36] perf: simplify schema shape dispatch --- gems/smithy-cbor/lib/smithy-cbor/builder.rb | 16 +++-- gems/smithy-cbor/lib/smithy-cbor/parser.rb | 23 ++++--- .../lib/smithy-client/default_params.rb | 14 ++-- .../lib/smithy-client/http_extension.rb | 5 +- .../lib/smithy-client/log_param_filter.rb | 10 +-- .../lib/smithy-client/param_converter.rb | 15 ++-- .../lib/smithy-client/param_validator.rb | 31 +++++---- .../spec/smithy-client/http_extension_spec.rb | 2 +- gems/smithy-json/lib/smithy-json/builder.rb | 17 ++--- gems/smithy-json/lib/smithy-json/extension.rb | 4 +- gems/smithy-json/lib/smithy-json/parser.rb | 29 ++++---- .../spec/smithy-json/extension_spec.rb | 2 +- .../document_utils/deserializer.rb | 12 ++-- .../document_utils/serializer.rb | 5 +- .../lib/smithy-schema/extension.rb | 68 ++++--------------- .../sig/smithy-schema/extension.rbs | 1 - .../spec/smithy-schema/extension_spec.rb | 6 -- gems/smithy-xml/lib/smithy-xml/builder.rb | 15 ++-- gems/smithy-xml/lib/smithy-xml/extension.rb | 40 ++++++----- .../smithy-xml/lib/smithy-xml/parser/frame.rb | 2 +- .../spec/smithy-xml/extension_spec.rb | 2 +- 21 files changed, 141 insertions(+), 178 deletions(-) diff --git a/gems/smithy-cbor/lib/smithy-cbor/builder.rb b/gems/smithy-cbor/lib/smithy-cbor/builder.rb index ffda40d4f..64a3e7b65 100644 --- a/gems/smithy-cbor/lib/smithy-cbor/builder.rb +++ b/gems/smithy-cbor/lib/smithy-cbor/builder.rb @@ -17,12 +17,13 @@ def build(shape, data) private def build_shape(shape, value) - case Schema::Extension.target_shape(shape) - when Schema::Extension::SHAPE_BLOB then blob(value) - when Schema::Extension::SHAPE_LIST then list(shape, value) - when Schema::Extension::SHAPE_MAP then map(shape, value) - when Schema::Extension::SHAPE_STRUCTURE then structure(shape, value) - when Schema::Extension::SHAPE_UNION then union(shape, value) + target = shape.target + case target + when Schema::Shapes::BlobShape then blob(value) + when Schema::Shapes::ListShape then list(shape, value) + when Schema::Shapes::MapShape then map(shape, value) + when Schema::Shapes::StructureShape then structure(shape, value) + when Schema::Shapes::UnionShape then union(shape, value) else value end end @@ -52,9 +53,10 @@ def map(shape, values) def structure(shape, values) return if values.nil? + target = shape.target values.each_pair.with_object({}) do |(member_name, value), data| next if value.nil? - next unless (member_shape = shape.target.member(member_name)) + next unless (member_shape = target.member(member_name)) data[member_shape.name] = build_shape(member_shape, value) end diff --git a/gems/smithy-cbor/lib/smithy-cbor/parser.rb b/gems/smithy-cbor/lib/smithy-cbor/parser.rb index b9cc03e91..bdb69906a 100644 --- a/gems/smithy-cbor/lib/smithy-cbor/parser.rb +++ b/gems/smithy-cbor/lib/smithy-cbor/parser.rb @@ -19,11 +19,12 @@ def parse(shape, bytes, result = nil) def parse_shape(shape, value, result = nil) return nil if value.nil? - case Schema::Extension.target_shape(shape) - when Schema::Extension::SHAPE_LIST then list(shape, value, result) - when Schema::Extension::SHAPE_MAP then map(shape, value, result) - when Schema::Extension::SHAPE_STRUCTURE then structure(shape, value, result) - when Schema::Extension::SHAPE_UNION then union(shape, value, result) + target = shape.target + case target + when Schema::Shapes::ListShape then list(shape, value, result) + when Schema::Shapes::MapShape then map(shape, value, result) + when Schema::Shapes::StructureShape then structure(shape, value, result) + when Schema::Shapes::UnionShape then union(shape, value, result) else value end end @@ -55,8 +56,9 @@ def map(shape, values, result = nil) end def structure(shape, values, result = nil) - result = shape.target.type.new if result.nil? - index = Schema::Extension.wire_index(shape.target) + target = shape.target + result = target.type.new if result.nil? + index = Schema::Extension.wire_index(target) values.each do |wire_name, value| next if value.nil? @@ -70,7 +72,8 @@ def structure(shape, values, result = nil) end def union(shape, values, result = nil) # rubocop:disable Metrics/AbcSize - index = Schema::Extension.wire_index(shape.target) + target = shape.target + index = Schema::Extension.wire_index(target) values.each do |wire_name, value| next if value.nil? @@ -78,13 +81,13 @@ def union(shape, values, result = nil) # rubocop:disable Metrics/AbcSize next unless entry member_name, member_shape = entry - result = shape.target.member_type(member_name) if result.nil? + result = target.member_type(member_name) if result.nil? return result.new(member_name => parse_shape(member_shape, value)) end values.delete('__type') key, value = values.first - shape.target.member_type(:unknown).new(unknown: { key => value }) + target.member_type(:unknown).new(unknown: { key => value }) end end end diff --git a/gems/smithy-client/lib/smithy-client/default_params.rb b/gems/smithy-client/lib/smithy-client/default_params.rb index b19b076d1..189d3c121 100644 --- a/gems/smithy-client/lib/smithy-client/default_params.rb +++ b/gems/smithy-client/lib/smithy-client/default_params.rb @@ -20,10 +20,10 @@ def apply(params) private def apply_shape(shape, value) - case Schema::Extension.target_shape(shape) - when Schema::Extension::SHAPE_LIST then list(shape, value) - when Schema::Extension::SHAPE_MAP then map(shape, value) - when Schema::Extension::SHAPE_STRUCTURE then structure(shape, value) + case shape.target + when Schema::Shapes::ListShape then list(shape, value) + when Schema::Shapes::MapShape then map(shape, value) + when Schema::Shapes::StructureShape then structure(shape, value) else value end end @@ -70,9 +70,9 @@ def default?(shape, traits) def default(member_shape) default = member_shape.traits['smithy.api#default'] - case Schema::Extension.target_shape(member_shape) - when Schema::Extension::SHAPE_BLOB then Base64.strict_decode64(default) - when Schema::Extension::SHAPE_TIMESTAMP then timestamp_default(default) + case member_shape.target + when Schema::Shapes::BlobShape then Base64.strict_decode64(default) + when Schema::Shapes::TimestampShape then timestamp_default(default) else default end end diff --git a/gems/smithy-client/lib/smithy-client/http_extension.rb b/gems/smithy-client/lib/smithy-client/http_extension.rb index 9229c08d9..96f18359a 100644 --- a/gems/smithy-client/lib/smithy-client/http_extension.rb +++ b/gems/smithy-client/lib/smithy-client/http_extension.rb @@ -189,8 +189,9 @@ def payload_type(member) end def content_type(member) - Schema::Extension.media_type(member.target) || - case member.target + target = member.target + Schema::Extension.media_type(target) || + case target when Schema::Shapes::BlobShape then 'application/octet-stream' when Schema::Shapes::StringShape, Schema::Shapes::EnumShape then 'text/plain' end diff --git a/gems/smithy-client/lib/smithy-client/log_param_filter.rb b/gems/smithy-client/lib/smithy-client/log_param_filter.rb index 5822b8143..4816242bf 100644 --- a/gems/smithy-client/lib/smithy-client/log_param_filter.rb +++ b/gems/smithy-client/lib/smithy-client/log_param_filter.rb @@ -9,11 +9,11 @@ def initialize(options = {}) end def filter(shape, values) - case Schema::Extension.target_shape(shape) - when Schema::Extension::SHAPE_LIST then list(shape, values) - when Schema::Extension::SHAPE_MAP then map(shape, values) - when Schema::Extension::SHAPE_STRUCTURE then structure(shape, values) - when Schema::Extension::SHAPE_UNION then union(shape, values) + case shape.target + when Schema::Shapes::ListShape then list(shape, values) + when Schema::Shapes::MapShape then map(shape, values) + when Schema::Shapes::StructureShape then structure(shape, values) + when Schema::Shapes::UnionShape then union(shape, values) else scalar(shape, values) end end diff --git a/gems/smithy-client/lib/smithy-client/param_converter.rb b/gems/smithy-client/lib/smithy-client/param_converter.rb index 326c32b0d..dd60f97a3 100644 --- a/gems/smithy-client/lib/smithy-client/param_converter.rb +++ b/gems/smithy-client/lib/smithy-client/param_converter.rb @@ -40,11 +40,11 @@ def c(shape, value) end def convert_shape(shape, value) - case Schema::Extension.target_shape(shape) - when Schema::Extension::SHAPE_LIST then list(shape, value) - when Schema::Extension::SHAPE_MAP then map(shape, value) - when Schema::Extension::SHAPE_STRUCTURE then structure(shape, value) - when Schema::Extension::SHAPE_UNION then union(shape, value) + case shape.target + when Schema::Shapes::ListShape then list(shape, value) + when Schema::Shapes::MapShape then map(shape, value) + when Schema::Shapes::StructureShape then structure(shape, value) + when Schema::Shapes::UnionShape then union(shape, value) else c(shape, value) end end @@ -72,11 +72,12 @@ def structure(shape, values) values = c(shape, values) return values unless values.respond_to?(:each_pair) + target = shape.target values.each_pair do |k, v| next if v.nil? - next unless shape.target.member?(k) + next unless target.member?(k) - values[k] = convert_shape(shape.target.member(k), v) + values[k] = convert_shape(target.member(k), v) end values end diff --git a/gems/smithy-client/lib/smithy-client/param_validator.rb b/gems/smithy-client/lib/smithy-client/param_validator.rb index dbf2a3fb9..a6f2a76cd 100644 --- a/gems/smithy-client/lib/smithy-client/param_validator.rb +++ b/gems/smithy-client/lib/smithy-client/param_validator.rb @@ -29,25 +29,25 @@ def validate!(params, context: 'params') # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity def validate_shape(shape, value, errors, context) - case Schema::Extension.target_shape(shape) - when Schema::Extension::SHAPE_STRUCTURE then structure(shape, value, errors, context) - when Schema::Extension::SHAPE_LIST then list(shape, value, errors, context) - when Schema::Extension::SHAPE_MAP then map(shape, value, errors, context) - when Schema::Extension::SHAPE_DOCUMENT then document(shape, value, errors, context) - when Schema::Extension::SHAPE_UNION then union(shape, value, errors, context) - when Schema::Extension::SHAPE_STRING, Schema::Extension::SHAPE_ENUM + case shape.target + when Schema::Shapes::StructureShape then structure(shape, value, errors, context) + when Schema::Shapes::ListShape then list(shape, value, errors, context) + when Schema::Shapes::MapShape then map(shape, value, errors, context) + when Schema::Shapes::DocumentShape then document(shape, value, errors, context) + when Schema::Shapes::UnionShape then union(shape, value, errors, context) + when Schema::Shapes::StringShape, Schema::Shapes::EnumShape errors << expected_got(context, 'a String', value) unless value.is_a?(String) - when Schema::Extension::SHAPE_INTEGER, Schema::Extension::SHAPE_INT_ENUM + when Schema::Shapes::IntegerShape, Schema::Shapes::IntEnumShape errors << expected_got(context, 'an Integer', value) unless value.is_a?(Integer) - when Schema::Extension::SHAPE_BIG_DECIMAL + when Schema::Shapes::BigDecimalShape errors << expected_got(context, 'a BigDecimal', value) unless value.is_a?(BigDecimal) - when Schema::Extension::SHAPE_FLOAT + when Schema::Shapes::FloatShape errors << expected_got(context, 'a Float', value) unless value.is_a?(Float) - when Schema::Extension::SHAPE_TIMESTAMP + when Schema::Shapes::TimestampShape errors << expected_got(context, 'a Time object', value) unless value.is_a?(Time) - when Schema::Extension::SHAPE_BOOLEAN + when Schema::Shapes::BooleanShape errors << expected_got(context, 'true or false', value) unless [true, false].include?(value) - when Schema::Extension::SHAPE_BLOB + when Schema::Shapes::BlobShape blob(shape, value, errors, context) end end @@ -116,8 +116,9 @@ def map(shape, values, errors, context) end def member(shape, name, value, errors, context) - if shape.target.member?(name) - member_shape = shape.target.member(name) + target = shape.target + if target.member?(name) + member_shape = target.member(name) validate_shape(member_shape, value, errors, context + "[#{name.inspect}]") else errors << "unexpected value at #{context}[#{name.inspect}]" diff --git a/gems/smithy-client/spec/smithy-client/http_extension_spec.rb b/gems/smithy-client/spec/smithy-client/http_extension_spec.rb index 6655e3537..917389df6 100644 --- a/gems/smithy-client/spec/smithy-client/http_extension_spec.rb +++ b/gems/smithy-client/spec/smithy-client/http_extension_spec.rb @@ -10,7 +10,7 @@ module Client expect(shape[:schema]).to be_nil expect(described_class.fetch(shape)).to eq({}) - expect(shape[:schema]).to eq(target_shape: Schema::Extension::SHAPE_STRING) + expect(shape[:schema]).to eq({}) end it 'caches HTTP operation metadata' do diff --git a/gems/smithy-json/lib/smithy-json/builder.rb b/gems/smithy-json/lib/smithy-json/builder.rb index 40176675d..157e6543b 100644 --- a/gems/smithy-json/lib/smithy-json/builder.rb +++ b/gems/smithy-json/lib/smithy-json/builder.rb @@ -19,14 +19,15 @@ def build(shape, data) private def build_shape(shape, value) # rubocop:disable Metrics/CyclomaticComplexity - case Schema::Extension.target_shape(shape) - when Schema::Extension::SHAPE_BLOB then blob(value) - when Schema::Extension::SHAPE_FLOAT then float(value) - when Schema::Extension::SHAPE_LIST then list(shape, value) - when Schema::Extension::SHAPE_MAP then map(shape, value) - when Schema::Extension::SHAPE_STRUCTURE then structure(shape, value) - when Schema::Extension::SHAPE_TIMESTAMP then timestamp(shape, value) - when Schema::Extension::SHAPE_UNION then union(shape, value) + target = shape.target + case target + when Schema::Shapes::BlobShape then blob(value) + when Schema::Shapes::FloatShape then float(value) + when Schema::Shapes::ListShape then list(shape, value) + when Schema::Shapes::MapShape then map(shape, value) + when Schema::Shapes::StructureShape then structure(shape, value) + when Schema::Shapes::TimestampShape then timestamp(shape, value) + when Schema::Shapes::UnionShape then union(shape, value) else value end end diff --git a/gems/smithy-json/lib/smithy-json/extension.rb b/gems/smithy-json/lib/smithy-json/extension.rb index b81d00c1a..9a42801bd 100644 --- a/gems/smithy-json/lib/smithy-json/extension.rb +++ b/gems/smithy-json/lib/smithy-json/extension.rb @@ -32,7 +32,7 @@ def fetch(shape) # # Example: # Extension.wire_index(shape) - # # => { 'wireName' => [:ruby_name, member, Schema::Extension::SHAPE_STRING] } + # # => { 'wireName' => [:ruby_name, member] } def wire_index(shape) (shape[KEY] || build_and_cache(shape))[:json_wire_index] end @@ -46,7 +46,7 @@ def wire_index(shape) # # Example: # Extension.member_index(shape) - # # => { ruby_name: ['wireName', member, Schema::Extension::SHAPE_STRING] } + # # => { ruby_name: ['wireName', member] } def member_index(shape) (shape[KEY] || build_and_cache(shape))[:json_member_index] end diff --git a/gems/smithy-json/lib/smithy-json/parser.rb b/gems/smithy-json/lib/smithy-json/parser.rb index b94c3512b..f1031a446 100644 --- a/gems/smithy-json/lib/smithy-json/parser.rb +++ b/gems/smithy-json/lib/smithy-json/parser.rb @@ -20,14 +20,15 @@ def parse(shape, bytes, result = nil) private def parse_shape(shape, value, result = nil) # rubocop:disable Metrics/CyclomaticComplexity - case Schema::Extension.target_shape(shape) - when Schema::Extension::SHAPE_BLOB then Base64.decode64(value) - when Schema::Extension::SHAPE_FLOAT then float(value) - when Schema::Extension::SHAPE_LIST then list(shape, value, result) - when Schema::Extension::SHAPE_MAP then map(shape, value, result) - when Schema::Extension::SHAPE_STRUCTURE then structure(shape, value, result) - when Schema::Extension::SHAPE_TIMESTAMP then timestamp(value) - when Schema::Extension::SHAPE_UNION then union(shape, value, result) + target = shape.target + case target + when Schema::Shapes::BlobShape then Base64.decode64(value) + when Schema::Shapes::FloatShape then float(value) + when Schema::Shapes::ListShape then list(shape, value, result) + when Schema::Shapes::MapShape then map(shape, value, result) + when Schema::Shapes::StructureShape then structure(shape, value, result) + when Schema::Shapes::TimestampShape then timestamp(value) + when Schema::Shapes::UnionShape then union(shape, value, result) else value end end @@ -72,8 +73,9 @@ def map(shape, values, result = nil) def structure(shape, values, result = nil) return if values.nil? - result = shape.target.type.new if result.nil? - index = @extension.wire_index(shape.target) + target = shape.target + result = target.type.new if result.nil? + index = @extension.wire_index(target) values.each do |wire_name, value| next if value.nil? @@ -100,7 +102,8 @@ def timestamp(value) end def union(shape, values, result = nil) # rubocop:disable Metrics/AbcSize - index = @extension.wire_index(shape.target) + target = shape.target + index = @extension.wire_index(target) values.each do |wire_name, value| next if value.nil? @@ -108,13 +111,13 @@ def union(shape, values, result = nil) # rubocop:disable Metrics/AbcSize next unless entry member_name, member_shape = entry - result = shape.target.member_type(member_name) if result.nil? + result = target.member_type(member_name) if result.nil? return result.new(member_name => parse_shape(member_shape, value)) end values.delete('__type') key, value = values.first - shape.target.member_type(:unknown).new(unknown: { key => value }) + target.member_type(:unknown).new(unknown: { key => value }) end end diff --git a/gems/smithy-json/spec/smithy-json/extension_spec.rb b/gems/smithy-json/spec/smithy-json/extension_spec.rb index ccd78f320..020ea249a 100644 --- a/gems/smithy-json/spec/smithy-json/extension_spec.rb +++ b/gems/smithy-json/spec/smithy-json/extension_spec.rb @@ -68,7 +68,7 @@ module Json expect(shape[:schema]).to be_nil expect(described_class.fetch(shape)).to be_empty expect(described_class.fetch(shape)).to be(shape[:json]) - expect(shape[:schema]).to eq(target_shape: Schema::Extension::SHAPE_STRING) + expect(shape[:schema]).to eq({}) 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 f2890ab2d..7520a20e3 100644 --- a/gems/smithy-schema/lib/smithy-schema/document_utils/deserializer.rb +++ b/gems/smithy-schema/lib/smithy-schema/document_utils/deserializer.rb @@ -74,8 +74,9 @@ def map(shape, values, result = nil) def structure(shape, values, result = nil) return if values.nil? - result = shape.target.type.new if result.nil? - shape.target.members.each do |member_name, member_shape| + target = shape.target + result = target.type.new if result.nil? + target.members.each do |member_name, member_shape| value = values[member_shape.name] result[member_name] = deserialize_shape(member_shape, value) unless value.nil? end @@ -100,17 +101,18 @@ def timestamp(value) end def union(shape, values, result = nil) # rubocop:disable Metrics/AbcSize - shape.target.members.each do |member_name, member_shape| + target = shape.target + target.members.each do |member_name, member_shape| value = values[member_shape.name] next if value.nil? - result = shape.target.member_type(member_name) if result.nil? + result = target.member_type(member_name) if result.nil? return result.new(member_name => deserialize_shape(member_shape, value)) end values.delete('__type') key, value = values.first - shape.target.member_type(:unknown).new(key, value) + target.member_type(:unknown).new(key, value) end end end diff --git a/gems/smithy-schema/lib/smithy-schema/document_utils/serializer.rb b/gems/smithy-schema/lib/smithy-schema/document_utils/serializer.rb index 96e24dd3e..bc169801b 100644 --- a/gems/smithy-schema/lib/smithy-schema/document_utils/serializer.rb +++ b/gems/smithy-schema/lib/smithy-schema/document_utils/serializer.rb @@ -160,9 +160,10 @@ def normalize_timestamp_value(value) end def resolve_member_shape(shape, name) - return shape.target.member(name) if shape.target.member?(name) + target = shape.target + return target.member(name) if target.member?(name) - shape.target.members.values.find do |member_shape| + target.members.values.find do |member_shape| member_shape.traits['smithy.api#jsonName'] == name || member_shape.name == name end end diff --git a/gems/smithy-schema/lib/smithy-schema/extension.rb b/gems/smithy-schema/lib/smithy-schema/extension.rb index 460779fff..93d0161c0 100644 --- a/gems/smithy-schema/lib/smithy-schema/extension.rb +++ b/gems/smithy-schema/lib/smithy-schema/extension.rb @@ -5,51 +5,19 @@ module Schema # Cached schema metadata shared by Smithy protocol codecs. # # Raw Smithy trait data remains on +shape.traits+ and +member.traits+ with - # string keys. This module resolves generic shape classification and - # modeled-member indexes. Protocol extensions own wire-specific metadata. + # string keys. This module resolves modeled-member indexes. Protocol + # extensions own wire-specific metadata. # @api private # rubocop:disable-next Metrics/ModuleLength module Extension 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 complete cached Schema metadata payload. # # Example: # Extension.fetch(shape) - # # => { target_shape: Extension::SHAPE_STRUCTURE, ... } + # # => { wire_index: ..., ... } def fetch(shape) shape[KEY] || build_and_cache(shape) end @@ -75,17 +43,6 @@ def member_index(shape) (shape[KEY] || build_and_cache(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) - (shape[KEY] || build_and_cache(shape))[:target_shape] - end - # Returns the modeled media type, when present. # # Example: @@ -228,9 +185,7 @@ def build_error_index(operation) end def build_shape_metadata(shape) - target = shape.target - target_shape = SHAPE_REF_BY_CLASS[target.class] - metadata = { target_shape: target_shape }.compact + metadata = {} add_media_type_metadata(metadata, shape) add_boolean_trait_metadata(metadata, shape) add_timestamp_metadata(metadata, shape) @@ -238,8 +193,7 @@ def build_shape_metadata(shape) end def build_member_metadata(member) - target_shape = SHAPE_REF_BY_CLASS[member.target.class] if member.target - metadata = { target_shape: target_shape }.compact + metadata = {} add_media_type_metadata(metadata, member) add_boolean_trait_metadata(metadata, member) add_timestamp_metadata(metadata, member) @@ -266,11 +220,12 @@ def build_aggregate_metadata(shape) 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) + target = member.target + next unless streaming_trait?(target) metadata[:streaming_member] ||= member - metadata[:event_stream_member] ||= member if member.target.class == Shapes::UnionShape - metadata[:streaming_member_unknown_length] ||= member unless requires_length_trait?(member.target) + metadata[:event_stream_member] ||= member if target.class == Shapes::UnionShape + metadata[:streaming_member_unknown_length] ||= member unless requires_length_trait?(target) end metadata[:wire_index] = wire_index.freeze @@ -281,11 +236,12 @@ def build_aggregate_metadata(shape) end def add_timestamp_metadata(metadata, shape) - return unless metadata[:target_shape] == SHAPE_TIMESTAMP + target = shape.target + return unless target.is_a?(Shapes::TimestampShape) metadata[:timestamp_format] = shape.traits['smithy.api#timestampFormat'] || - shape.target.traits['smithy.api#timestampFormat'] || + target.traits['smithy.api#timestampFormat'] || :default end diff --git a/gems/smithy-schema/sig/smithy-schema/extension.rbs b/gems/smithy-schema/sig/smithy-schema/extension.rbs index 5a3652c97..020cef1a4 100644 --- a/gems/smithy-schema/sig/smithy-schema/extension.rbs +++ b/gems/smithy-schema/sig/smithy-schema/extension.rbs @@ -4,7 +4,6 @@ module Smithy 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]] def self.member_index: ((Shapes::StructureShape | Shapes::UnionShape) shape) -> Hash[Symbol, [String, Shapes::MemberShape]] - def self.target_shape: ((Shapes::Shape | Shapes::MemberShape) shape) -> Integer? 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? diff --git a/gems/smithy-schema/spec/smithy-schema/extension_spec.rb b/gems/smithy-schema/spec/smithy-schema/extension_spec.rb index 9daa25ebf..5c4de5f4e 100644 --- a/gems/smithy-schema/spec/smithy-schema/extension_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/extension_spec.rb @@ -59,12 +59,6 @@ module Schema end 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 'resolves a member timestamp format before its target format' do timestamp = Shapes::TimestampShape.new( traits: { 'smithy.api#timestampFormat' => 'date-time' } diff --git a/gems/smithy-xml/lib/smithy-xml/builder.rb b/gems/smithy-xml/lib/smithy-xml/builder.rb index fec30239d..af46a357d 100644 --- a/gems/smithy-xml/lib/smithy-xml/builder.rb +++ b/gems/smithy-xml/lib/smithy-xml/builder.rb @@ -23,13 +23,14 @@ def build(shape, data, output = nil) private def build_shape(name, shape, value) - case Schema::Extension.target_shape(shape) - when Schema::Extension::SHAPE_BLOB then node(name, shape, blob(value)) - when Schema::Extension::SHAPE_LIST then list(name, shape, value) - when Schema::Extension::SHAPE_MAP then map(name, shape, value) - when Schema::Extension::SHAPE_STRUCTURE then structure(name, shape, value) - when Schema::Extension::SHAPE_TIMESTAMP then node(name, shape, timestamp(shape, value)) - when Schema::Extension::SHAPE_UNION then union(name, shape, value) + target_shape = shape.target + case target_shape + when Schema::Shapes::BlobShape then node(name, shape, blob(value)) + when Schema::Shapes::ListShape then list(name, shape, value) + when Schema::Shapes::MapShape then map(name, shape, value) + when Schema::Shapes::StructureShape then structure(name, shape, value) + when Schema::Shapes::TimestampShape then node(name, shape, timestamp(shape, value)) + when Schema::Shapes::UnionShape then union(name, shape, value) else node(name, shape, value.to_s) end end diff --git a/gems/smithy-xml/lib/smithy-xml/extension.rb b/gems/smithy-xml/lib/smithy-xml/extension.rb index 1ea8e6d04..1e9f845fc 100644 --- a/gems/smithy-xml/lib/smithy-xml/extension.rb +++ b/gems/smithy-xml/lib/smithy-xml/extension.rb @@ -109,13 +109,12 @@ def build_and_cache(shape) def build_shape_metadata(shape) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength target = shape.target - target_shape = Schema::Extension.target_shape(shape) metadata = { xml_structure_name: shape.traits['smithy.api#xmlName'] || target.name, xml_namespace_attrs: build_namespace_attrs(shape, target), - xml_frame_class: frame_class_for(target_shape, flattened?(shape)) + xml_frame_class: frame_class_for(target, flattened?(shape)) } - if [Schema::Extension::SHAPE_STRUCTURE, Schema::Extension::SHAPE_UNION].include?(target_shape) + if target.is_a?(Schema::Shapes::StructureShape) || target.is_a?(Schema::Shapes::UnionShape) members = { attributes: [], elements: [] } index = {} Schema::Extension.each_member(shape) do |ruby_name, member| @@ -138,11 +137,10 @@ def build_shape_metadata(shape) # rubocop:disable Metrics/AbcSize, Metrics/Metho def build_member_metadata(member) # rubocop:disable Metrics/AbcSize target = member.target - target_shape = Schema::Extension.target_shape(member) xml_name = member.traits['smithy.api#xmlName'] structure_name = xml_name || target.traits['smithy.api#xmlName'] if structure_name.nil? && - [Schema::Extension::SHAPE_STRUCTURE, Schema::Extension::SHAPE_UNION].include?(target_shape) + (target.is_a?(Schema::Shapes::StructureShape) || target.is_a?(Schema::Shapes::UnionShape)) structure_name = target.name end metadata = { @@ -150,14 +148,14 @@ def build_member_metadata(member) # rubocop:disable Metrics/AbcSize xml_wire_name: xml_name || member.name, xml_namespace_attrs: build_namespace_attrs(member, target), xml_attribute: member.traits.key?('smithy.api#xmlAttribute'), - xml_frame_class: frame_class_for(target_shape, flattened?(member)) + xml_frame_class: frame_class_for(target, flattened?(member)) } add_map_parts(metadata, target) metadata.freeze end def add_map_parts(metadata, target) - return unless Schema::Extension.target_shape(target) == Schema::Extension::SHAPE_MAP + return unless target.is_a?(Schema::Shapes::MapShape) key_member = target.key value_member = target.value @@ -180,26 +178,26 @@ def build_namespace_attrs(shape, target) end end - def frame_class_for(target_shape, flattened) - klass = base_frame_class(target_shape) + def frame_class_for(target, flattened) + klass = base_frame_class(target) return Parser::FlatListFrame if klass == Parser::ListFrame && flattened return Parser::MapEntryFrame if klass == Parser::MapFrame && flattened klass end - def base_frame_class(target_shape) # rubocop:disable Metrics/CyclomaticComplexity - case target_shape - when Schema::Extension::SHAPE_BIG_DECIMAL then Parser::BigDecimalFrame - when Schema::Extension::SHAPE_BLOB then Parser::BlobFrame - when Schema::Extension::SHAPE_BOOLEAN then Parser::BooleanFrame - when Schema::Extension::SHAPE_ENUM, Schema::Extension::SHAPE_STRING then Parser::StringFrame - when Schema::Extension::SHAPE_FLOAT then Parser::FloatFrame - when Schema::Extension::SHAPE_INTEGER, Schema::Extension::SHAPE_INT_ENUM then Parser::IntegerFrame - when Schema::Extension::SHAPE_LIST then Parser::ListFrame - when Schema::Extension::SHAPE_MAP then Parser::MapFrame - when Schema::Extension::SHAPE_STRUCTURE, Schema::Extension::SHAPE_UNION then Parser::StructureFrame - when Schema::Extension::SHAPE_TIMESTAMP then Parser::TimestampFrame + def base_frame_class(target) # rubocop:disable Metrics/CyclomaticComplexity + case target + when Schema::Shapes::BigDecimalShape then Parser::BigDecimalFrame + when Schema::Shapes::BlobShape then Parser::BlobFrame + when Schema::Shapes::BooleanShape then Parser::BooleanFrame + when Schema::Shapes::EnumShape, Schema::Shapes::StringShape then Parser::StringFrame + when Schema::Shapes::FloatShape then Parser::FloatFrame + when Schema::Shapes::IntegerShape, Schema::Shapes::IntEnumShape then Parser::IntegerFrame + when Schema::Shapes::ListShape then Parser::ListFrame + when Schema::Shapes::MapShape then Parser::MapFrame + when Schema::Shapes::StructureShape, Schema::Shapes::UnionShape then Parser::StructureFrame + when Schema::Shapes::TimestampShape then Parser::TimestampFrame end end end diff --git a/gems/smithy-xml/lib/smithy-xml/parser/frame.rb b/gems/smithy-xml/lib/smithy-xml/parser/frame.rb index d664cb42a..3c3b5ec84 100644 --- a/gems/smithy-xml/lib/smithy-xml/parser/frame.rb +++ b/gems/smithy-xml/lib/smithy-xml/parser/frame.rb @@ -236,7 +236,7 @@ def child_frame(xml_name) if (@member = @members[xml_name]) _member_name, member_shape = @member Frame.new(xml_name, self, member_shape) - elsif Schema::Extension.target_shape(@shape) == Schema::Extension::SHAPE_UNION + elsif @shape.target.is_a?(Schema::Shapes::UnionShape) UnknownMemberFrame.new(xml_name, self, nil, @result) else NullFrame.new(xml_name, self) diff --git a/gems/smithy-xml/spec/smithy-xml/extension_spec.rb b/gems/smithy-xml/spec/smithy-xml/extension_spec.rb index 6087f42fd..7af9a2203 100644 --- a/gems/smithy-xml/spec/smithy-xml/extension_spec.rb +++ b/gems/smithy-xml/spec/smithy-xml/extension_spec.rb @@ -47,7 +47,7 @@ module Xml expect(shape[:schema]).to be_nil described_class.fetch(shape) - expect(shape[:schema]).to eq(target_shape: Schema::Extension::SHAPE_STRING) + expect(shape[:schema]).to eq({}) end end From 90d937a58a9541876023d0ae0e2afbbf84871b2f Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 21 Sep 2026 11:03:59 -0700 Subject: [PATCH 05/36] fix: avoid reusing serde workers across calls --- gems/smithy-cbor/lib/smithy-cbor/codec.rb | 7 +++---- gems/smithy-json/lib/smithy-json/codec.rb | 7 +++---- gems/smithy-xml/lib/smithy-xml/codec.rb | 7 +++---- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/gems/smithy-cbor/lib/smithy-cbor/codec.rb b/gems/smithy-cbor/lib/smithy-cbor/codec.rb index b1d9b7042..2c95508e0 100644 --- a/gems/smithy-cbor/lib/smithy-cbor/codec.rb +++ b/gems/smithy-cbor/lib/smithy-cbor/codec.rb @@ -6,15 +6,14 @@ module Cbor class Codec # @param [Hash] options def initialize(options = {}) - @builder = Builder.new(options) - @parser = Parser.new(options) + @options = options end # @param [Shape] shape # @param [Object] data # @return [String, nil] def build(shape, data) - @builder.build(shape, data) + Builder.new(@options).build(shape, data) end # @param [Shape] shape @@ -22,7 +21,7 @@ def build(shape, data) # @param [Object, nil] result (nil) # @return [Object, nil] def parse(shape, bytes, result = nil) - @parser.parse(shape, bytes, result) + Parser.new(@options).parse(shape, bytes, result) end end end diff --git a/gems/smithy-json/lib/smithy-json/codec.rb b/gems/smithy-json/lib/smithy-json/codec.rb index d67a1ea5f..0ad2c6d3c 100644 --- a/gems/smithy-json/lib/smithy-json/codec.rb +++ b/gems/smithy-json/lib/smithy-json/codec.rb @@ -6,15 +6,14 @@ module Json class Codec # @param [Hash] options def initialize(options = {}) - @builder = Builder.new(options) - @parser = Parser.new(options) + @options = options end # @param [Shape] shape # @param [Object] data # @return [String, nil] def build(shape, data) - @builder.build(shape, data) + Builder.new(@options).build(shape, data) end # @param [Shape] shape @@ -22,7 +21,7 @@ def build(shape, data) # @param [Object, nil] result (nil) # @return [Object, nil] def parse(shape, bytes, result = nil) - @parser.parse(shape, bytes, result) + Parser.new(@options).parse(shape, bytes, result) end end end diff --git a/gems/smithy-xml/lib/smithy-xml/codec.rb b/gems/smithy-xml/lib/smithy-xml/codec.rb index 9d67125e6..23ae8d86e 100644 --- a/gems/smithy-xml/lib/smithy-xml/codec.rb +++ b/gems/smithy-xml/lib/smithy-xml/codec.rb @@ -6,8 +6,7 @@ module Xml class Codec # @param [Hash] options def initialize(options = {}) - @builder = Builder.new(options) - @parser = Parser.new(options) + @options = options end # @param [Shape] shape @@ -15,7 +14,7 @@ def initialize(options = {}) # @param [Array, nil] output (nil) # @return [String, nil] def build(shape, data, output = nil) - @builder.build(shape, data, output) + Builder.new(@options).build(shape, data, output) end # @param [Shape] shape @@ -23,7 +22,7 @@ def build(shape, data, output = nil) # @param [Object, nil] result (nil) # @return [Object, nil] def parse(shape, bytes, result = nil) - @parser.parse(shape, bytes, result) + Parser.new(@options).parse(shape, bytes, result) end end end From b6271002769f907a9a545ca630e3ff1d841c78f9 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 21 Sep 2026 11:30:19 -0700 Subject: [PATCH 06/36] fix: align serde RBS contracts --- gems/smithy-cbor/lib/smithy-cbor/parser.rb | 2 +- gems/smithy-json/lib/smithy-json/builder.rb | 1 - gems/smithy-json/lib/smithy-json/parser.rb | 3 +-- gems/smithy-json/sig/smithy-json/extension.rbs | 2 +- .../lib/smithy-schema/document_utils/deserializer.rb | 2 +- gems/smithy-schema/lib/smithy-schema/extension.rb | 4 +--- gems/smithy-schema/sig/smithy-schema/shapes.rbs | 2 +- gems/smithy-schema/spec/smithy-schema/extension_spec.rb | 1 - 8 files changed, 6 insertions(+), 11 deletions(-) diff --git a/gems/smithy-cbor/lib/smithy-cbor/parser.rb b/gems/smithy-cbor/lib/smithy-cbor/parser.rb index bdb69906a..34dbb3e11 100644 --- a/gems/smithy-cbor/lib/smithy-cbor/parser.rb +++ b/gems/smithy-cbor/lib/smithy-cbor/parser.rb @@ -71,7 +71,7 @@ def structure(shape, values, result = nil) result end - def union(shape, values, result = nil) # rubocop:disable Metrics/AbcSize + def union(shape, values, result = nil) target = shape.target index = Schema::Extension.wire_index(target) values.each do |wire_name, value| diff --git a/gems/smithy-json/lib/smithy-json/builder.rb b/gems/smithy-json/lib/smithy-json/builder.rb index 157e6543b..0f7bb8582 100644 --- a/gems/smithy-json/lib/smithy-json/builder.rb +++ b/gems/smithy-json/lib/smithy-json/builder.rb @@ -107,7 +107,6 @@ def union(shape, values) wire_name, member_shape = entry { wire_name => build_shape(member_shape, value) } end - end end end diff --git a/gems/smithy-json/lib/smithy-json/parser.rb b/gems/smithy-json/lib/smithy-json/parser.rb index f1031a446..22ec9fc87 100644 --- a/gems/smithy-json/lib/smithy-json/parser.rb +++ b/gems/smithy-json/lib/smithy-json/parser.rb @@ -101,7 +101,7 @@ def timestamp(value) end end - def union(shape, values, result = nil) # rubocop:disable Metrics/AbcSize + def union(shape, values, result = nil) target = shape.target index = @extension.wire_index(target) values.each do |wire_name, value| @@ -119,7 +119,6 @@ def union(shape, values, result = nil) # rubocop:disable Metrics/AbcSize key, value = values.first target.member_type(:unknown).new(unknown: { key => value }) end - end end end diff --git a/gems/smithy-json/sig/smithy-json/extension.rbs b/gems/smithy-json/sig/smithy-json/extension.rbs index 322381107..6c947789b 100644 --- a/gems/smithy-json/sig/smithy-json/extension.rbs +++ b/gems/smithy-json/sig/smithy-json/extension.rbs @@ -4,7 +4,7 @@ module Smithy type aggregate_shape = Schema::Shapes::StructureShape | Schema::Shapes::UnionShape type serde_shape = aggregate_shape | Schema::Shapes::MemberShape - def self.fetch: (serde_shape shape) -> Hash[Symbol, untyped] + def self.fetch: ((Schema::Shapes::Shape | Schema::Shapes::MemberShape) shape) -> Hash[Symbol, untyped] def self.wire_index: (aggregate_shape shape) -> Hash[String?, [Symbol, Schema::Shapes::MemberShape]] def self.member_index: (aggregate_shape shape) -> Hash[Symbol, [String?, Schema::Shapes::MemberShape]] def self.timestamp_format: ((Schema::Shapes::Shape | Schema::Shapes::MemberShape) shape) -> (String | Symbol) 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 7520a20e3..01872ea5d 100644 --- a/gems/smithy-schema/lib/smithy-schema/document_utils/deserializer.rb +++ b/gems/smithy-schema/lib/smithy-schema/document_utils/deserializer.rb @@ -100,7 +100,7 @@ def timestamp(value) end end - def union(shape, values, result = nil) # rubocop:disable Metrics/AbcSize + def union(shape, values, result = nil) target = shape.target target.members.each do |member_name, member_shape| value = values[member_shape.name] diff --git a/gems/smithy-schema/lib/smithy-schema/extension.rb b/gems/smithy-schema/lib/smithy-schema/extension.rb index 93d0161c0..603ddc8b6 100644 --- a/gems/smithy-schema/lib/smithy-schema/extension.rb +++ b/gems/smithy-schema/lib/smithy-schema/extension.rb @@ -8,7 +8,6 @@ module Schema # string keys. This module resolves modeled-member indexes. Protocol # extensions own wire-specific metadata. # @api private - # rubocop:disable-next Metrics/ModuleLength module Extension KEY = :schema @@ -224,7 +223,7 @@ def build_aggregate_metadata(shape) next unless streaming_trait?(target) metadata[:streaming_member] ||= member - metadata[:event_stream_member] ||= member if target.class == Shapes::UnionShape + metadata[:event_stream_member] ||= member if target.instance_of?(Shapes::UnionShape) metadata[:streaming_member_unknown_length] ||= member unless requires_length_trait?(target) end @@ -263,7 +262,6 @@ def streaming_trait?(shape) def requires_length_trait?(shape) shape.traits.key?('smithy.api#requiresLength') end - end end end diff --git a/gems/smithy-schema/sig/smithy-schema/shapes.rbs b/gems/smithy-schema/sig/smithy-schema/shapes.rbs index 38ed6e212..6760a72cc 100644 --- a/gems/smithy-schema/sig/smithy-schema/shapes.rbs +++ b/gems/smithy-schema/sig/smithy-schema/shapes.rbs @@ -6,7 +6,7 @@ module Smithy attr_reader target: Shape attr_accessor id: String - attr_accessor name: String + attr_accessor name: String? attr_accessor traits: Hash[String, untyped] def []: (Symbol) -> Object def key?: (Symbol) -> bool diff --git a/gems/smithy-schema/spec/smithy-schema/extension_spec.rb b/gems/smithy-schema/spec/smithy-schema/extension_spec.rb index 5c4de5f4e..38db5e56f 100644 --- a/gems/smithy-schema/spec/smithy-schema/extension_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/extension_spec.rb @@ -79,7 +79,6 @@ module Schema expect(described_class.media_type(shape)).to eq('application/custom') end - end end end From 89ce4466e41d7fb0dd8ba4d5bf0b699d1f122077 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 21 Sep 2026 13:14:10 -0700 Subject: [PATCH 07/36] perf: optimize CBOR decoder reads --- gems/smithy-cbor/lib/smithy-cbor/decoder.rb | 63 +++++++++++++++------ 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/gems/smithy-cbor/lib/smithy-cbor/decoder.rb b/gems/smithy-cbor/lib/smithy-cbor/decoder.rb index 72779550e..c7343b2f3 100644 --- a/gems/smithy-cbor/lib/smithy-cbor/decoder.rb +++ b/gems/smithy-cbor/lib/smithy-cbor/decoder.rb @@ -36,7 +36,7 @@ def decode_item # rubocop:disable Metrics @depth += 1 raise ParseError, "Maximum nesting depth (#{MAX_DEPTH}) exceeded" if @depth > MAX_DEPTH - case (next_type = peek_type) + case peek_type when :array read_array.times.map { decode_item } when :map @@ -47,22 +47,24 @@ def decode_item # rubocop:disable Metrics when :indefinite_string then process_indefinite_string when :tag then process_tag when :break_stop_code then raise ParseError, 'Unexpected break code' - else send("read_#{next_type}") + when :integer then read_integer + when :binary_string then read_binary_string + when :string then read_string + when :boolean then read_boolean + when :nil then read_nil + when :undefined then read_undefined + when :half then read_half + when :float then read_float + when :double then read_double + when :reserved_undefined then read_reserved_undefined end ensure @depth -= 1 end - def peek(n_bytes) - return @buffer[@pos, n_bytes] if (@pos + n_bytes) <= @buffer.bytesize - - left = @buffer.bytesize - @pos - raise ParseError, "Out of bytes. Trying to read #{n_bytes} bytes but buffer contains only #{left}" - end - # low level streaming interface def peek_type # rubocop:disable Metrics - ib = peek(1).ord + ib = peek_byte add_info = ib & FIVE_BIT_MASK major_type = ib >> 5 case major_type @@ -191,17 +193,17 @@ def read_binary_string def read_count(add_info) case add_info when 0..23 then add_info - when 24 then take(1).ord - when 25 then take(2).unpack1('n') - when 26 then take(4).unpack1('N') - when 27 then take(8).unpack1('Q>') + when 24 then read_byte + when 25 then unpack1('n', 2) + when 26 then unpack1('N', 4) + when 27 then unpack1('Q>', 8) else raise ParseError, "Unexpected additional information: #{add_info}" end end def read_double read_info - take(8).unpack1('G') + unpack1('G', 8) end # returns nothing but consumes and checks the type/info. @@ -211,7 +213,7 @@ def read_end_indefinite_collection def read_float read_info - take(4).unpack1('g') + unpack1('g', 4) end # 16 bit IEEE 754 half-precision floats @@ -222,7 +224,7 @@ def read_float # precision - 10 bits def read_half read_info - b16 = take(2).unpack1('n') + b16 = unpack1('n', 2) exp = (b16 >> 10) & 0x1f mant = b16 & 0x3ff val = @@ -245,7 +247,7 @@ def read_half # return a tuple of major_type, add_info def read_info - ib = take(1).ord + ib = read_byte [ib >> 5, ib & FIVE_BIT_MASK] end @@ -305,6 +307,31 @@ def read_undefined :undefined end + def peek_byte + byte = @buffer.getbyte(@pos) + return byte unless byte.nil? + + left = @buffer.bytesize - @pos + raise ParseError, "Out of bytes. Trying to read 1 bytes but buffer contains only #{left}" + end + + def read_byte + byte = peek_byte + @pos += 1 + byte + end + + def unpack1(format, n_bytes) + if (@pos + n_bytes) > @buffer.bytesize + left = @buffer.bytesize - @pos + raise ParseError, "Out of bytes. Trying to read #{n_bytes} bytes but buffer contains only #{left}" + end + + value = @buffer.unpack1(format, offset: @pos) + @pos += n_bytes + value + end + def take(n_bytes) opos = @pos @pos += n_bytes From 18c65ba3c1a10c3db20b778d55653357ab4785c3 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 21 Sep 2026 13:16:33 -0700 Subject: [PATCH 08/36] perf: decode CBOR headers once --- gems/smithy-cbor/lib/smithy-cbor/decoder.rb | 146 +++++--------------- 1 file changed, 36 insertions(+), 110 deletions(-) diff --git a/gems/smithy-cbor/lib/smithy-cbor/decoder.rb b/gems/smithy-cbor/lib/smithy-cbor/decoder.rb index c7343b2f3..1e6e92b37 100644 --- a/gems/smithy-cbor/lib/smithy-cbor/decoder.rb +++ b/gems/smithy-cbor/lib/smithy-cbor/decoder.rb @@ -36,48 +36,26 @@ def decode_item # rubocop:disable Metrics @depth += 1 raise ParseError, "Maximum nesting depth (#{MAX_DEPTH}) exceeded" if @depth > MAX_DEPTH - case peek_type - when :array - read_array.times.map { decode_item } - when :map - read_map.times.to_h { [read_string, decode_item] } - when :indefinite_array then process_indefinite_array - when :indefinite_map then process_indefinite_map - when :indefinite_binary_string then process_indefinite_binary - when :indefinite_string then process_indefinite_string - when :tag then process_tag - when :break_stop_code then raise ParseError, 'Unexpected break code' - when :integer then read_integer - when :binary_string then read_binary_string - when :string then read_string - when :boolean then read_boolean - when :nil then read_nil - when :undefined then read_undefined - when :half then read_half - when :float then read_float - when :double then read_double - when :reserved_undefined then read_reserved_undefined - end + ib = read_byte + add_info = ib & FIVE_BIT_MASK + process_item(ib >> 5, add_info) ensure @depth -= 1 end - # low level streaming interface - def peek_type # rubocop:disable Metrics - ib = peek_byte - add_info = ib & FIVE_BIT_MASK - major_type = ib >> 5 + def process_item(major_type, add_info) # rubocop:disable Metrics case major_type - when 0, 1 then :integer + when 0 then read_count(add_info) + when 1 then -1 - read_count(add_info) when 2 - add_info == 31 ? :indefinite_binary_string : :binary_string + add_info == 31 ? process_indefinite_binary : read_binary_string(add_info) when 3 - add_info == 31 ? :indefinite_string : :string + add_info == 31 ? process_indefinite_string : read_string(add_info) when 4 - add_info == 31 ? :indefinite_array : :array + add_info == 31 ? process_indefinite_array : read_array(add_info).times.map { decode_item } when 5 - add_info == 31 ? :indefinite_map : :map - when 6 then :tag + add_info == 31 ? process_indefinite_map : read_map(add_info).times.to_h { [read_string, decode_item] } + when 6 then process_tag(add_info) when 7 then process_major_type_simple(add_info) end end @@ -85,51 +63,48 @@ def peek_type # rubocop:disable Metrics # simple or float def process_major_type_simple(add_info) # rubocop:disable Metrics case add_info - when 20, 21 then :boolean - when 22 then :nil - when 23 then :undefined # for smithy, this should be parsed as nil - when 25 then :half - when 26 then :float - when 27 then :double - when 31 then :break_stop_code - else :reserved_undefined + when 20 then false + when 21 then true + when 22 then nil + when 23 then :undefined + when 25 then read_half + when 26 then unpack1('g', 4) + when 27 then unpack1('G', 8) + when 31 then raise ParseError, 'Unexpected break code' + else raise ParseError, "Undefined reserved additional information: #{add_info}" end end def process_indefinite_array - read_start_indefinite_array value = [] - value << decode_item until peek_type == :break_stop_code + value << decode_item until break_stop_code? read_end_indefinite_collection value end def process_indefinite_binary - read_info value = String.new - value << read_binary_string until peek_type == :break_stop_code + value << read_binary_string until break_stop_code? read_end_indefinite_collection value end def process_indefinite_map - read_start_indefinite_map value = {} - value[read_string] = decode_item until peek_type == :break_stop_code + value[read_string] = decode_item until break_stop_code? read_end_indefinite_collection value end def process_indefinite_string - read_info value = String.new - value << read_string until peek_type == :break_stop_code + value << read_string until break_stop_code? read_end_indefinite_collection value.force_encoding(Encoding::UTF_8) end - def process_tag - case (tag = read_tag) + def process_tag(add_info) + case (tag = read_count(add_info)) when TAG_TYPE_EPOCH item = decode_item Time.at(item) @@ -144,8 +119,8 @@ def process_tag # returns only the length of the array, caller must read the correct # number of values after this - def read_array - _major_type, add_info = read_info + def read_array(add_info = nil) + _major_type, add_info = read_info if add_info.nil? read_count(add_info) end @@ -177,16 +152,8 @@ def read_bignum(tag_value) end end - def read_boolean - _major_type, add_info = read_info - case add_info - when 20 then false - when 21 then true - end - end - - def read_binary_string - _major_type, add_info = read_info + def read_binary_string(add_info = nil) + _major_type, add_info = read_info if add_info.nil? take(read_count(add_info)).force_encoding(Encoding::BINARY) end @@ -201,21 +168,11 @@ def read_count(add_info) end end - def read_double - read_info - unpack1('G', 8) - end - # returns nothing but consumes and checks the type/info. def read_end_indefinite_collection read_info end - def read_float - read_info - unpack1('g', 4) - end - # 16 bit IEEE 754 half-precision floats # Support decoding only # format: @@ -223,7 +180,6 @@ def read_float # exponent - 5 bits # precision - 10 bits def read_half - read_info b16 = unpack1('n', 2) exp = (b16 >> 10) & 0x1f mant = b16 & 0x3ff @@ -261,50 +217,20 @@ def read_integer end end - def read_nil - read_info - nil - end - # returns only the length of the array, caller must read the correct # number of key value pairs after this - def read_map - _major_type, add_info = read_info + def read_map(add_info = nil) + _major_type, add_info = read_info if add_info.nil? read_count(add_info) end - # returns nothing but consumes and checks the type/info. - # Caller must keep reading until encountering the stop sequence - def read_start_indefinite_array - read_info - end - - # returns nothing but consumes and checks the type/info. - # Caller must keep reading until encountering the stop sequence - def read_start_indefinite_map - read_info - end - - def read_string - _major_type, add_info = read_info + def read_string(add_info = nil) + _major_type, add_info = read_info if add_info.nil? take(read_count(add_info)).force_encoding(Encoding::UTF_8) end - # returns only the tag, caller must interpret the tag and read another - # value as appropriate - def read_tag - _major_type, add_info = read_info - read_count(add_info) - end - - def read_reserved_undefined - _major_type, add_info = read_info - raise ParseError, "Undefined reserved additional information: #{add_info}" - end - - def read_undefined - read_info - :undefined + def break_stop_code? + peek_byte == 0xFF end def peek_byte From 049f52c2765f5933fe6411b39d00a53522c39f3e Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 21 Sep 2026 13:35:23 -0700 Subject: [PATCH 09/36] perf: reduce CBOR collection allocations --- gems/smithy-cbor/lib/smithy-cbor/decoder.rb | 45 +++++++++++---------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/gems/smithy-cbor/lib/smithy-cbor/decoder.rb b/gems/smithy-cbor/lib/smithy-cbor/decoder.rb index 1e6e92b37..952b4d461 100644 --- a/gems/smithy-cbor/lib/smithy-cbor/decoder.rb +++ b/gems/smithy-cbor/lib/smithy-cbor/decoder.rb @@ -52,14 +52,27 @@ def process_item(major_type, add_info) # rubocop:disable Metrics when 3 add_info == 31 ? process_indefinite_string : read_string(add_info) when 4 - add_info == 31 ? process_indefinite_array : read_array(add_info).times.map { decode_item } + add_info == 31 ? process_indefinite_array : process_array(add_info) when 5 - add_info == 31 ? process_indefinite_map : read_map(add_info).times.to_h { [read_string, decode_item] } + add_info == 31 ? process_indefinite_map : process_map(add_info) when 6 then process_tag(add_info) when 7 then process_major_type_simple(add_info) end end + def process_array(add_info) + count = read_array(add_info) + value = Array.new(count) + count.times { |index| value[index] = decode_item } + value + end + + def process_map(add_info) + value = {} + read_map(add_info).times { value[read_string] = decode_item } + value + end + # simple or float def process_major_type_simple(add_info) # rubocop:disable Metrics case add_info @@ -120,7 +133,7 @@ def process_tag(add_info) # returns only the length of the array, caller must read the correct # number of values after this def read_array(add_info = nil) - _major_type, add_info = read_info if add_info.nil? + add_info = read_byte & FIVE_BIT_MASK if add_info.nil? read_count(add_info) end @@ -140,7 +153,7 @@ def read_big_decimal # tag type 2 or 3 def read_bignum(tag_value) - _major_type, add_info = read_info + add_info = read_byte & FIVE_BIT_MASK bstr = take(read_count(add_info)) v = bstr.bytes.inject(0) do |sum, b| sum <<= 8 @@ -153,7 +166,7 @@ def read_bignum(tag_value) end def read_binary_string(add_info = nil) - _major_type, add_info = read_info if add_info.nil? + add_info = read_byte & FIVE_BIT_MASK if add_info.nil? take(read_count(add_info)).force_encoding(Encoding::BINARY) end @@ -170,7 +183,7 @@ def read_count(add_info) # returns nothing but consumes and checks the type/info. def read_end_indefinite_collection - read_info + read_byte end # 16 bit IEEE 754 half-precision floats @@ -201,31 +214,21 @@ def read_half end end - # return a tuple of major_type, add_info - def read_info - ib = read_byte - [ib >> 5, ib & FIVE_BIT_MASK] - end - def read_integer - major_type, add_info = read_info - - val = read_count(add_info) - case major_type - when 0 then val - when 1 then -1 - val - end + ib = read_byte + val = read_count(ib & FIVE_BIT_MASK) + (ib >> 5).zero? ? val : -1 - val end # returns only the length of the array, caller must read the correct # number of key value pairs after this def read_map(add_info = nil) - _major_type, add_info = read_info if add_info.nil? + add_info = read_byte & FIVE_BIT_MASK if add_info.nil? read_count(add_info) end def read_string(add_info = nil) - _major_type, add_info = read_info if add_info.nil? + add_info = read_byte & FIVE_BIT_MASK if add_info.nil? take(read_count(add_info)).force_encoding(Encoding::UTF_8) end From 45461aa2dfad2ba26c218ca972bb88491dbb485b Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 21 Sep 2026 13:46:43 -0700 Subject: [PATCH 10/36] perf: cache CBOR byte headers --- gems/smithy-cbor/lib/smithy-cbor/encoder.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gems/smithy-cbor/lib/smithy-cbor/encoder.rb b/gems/smithy-cbor/lib/smithy-cbor/encoder.rb index 02b0bbceb..58dcb6931 100644 --- a/gems/smithy-cbor/lib/smithy-cbor/encoder.rb +++ b/gems/smithy-cbor/lib/smithy-cbor/encoder.rb @@ -24,6 +24,7 @@ class Encoder TAG_TYPE_BIGDEC = 4 MAX_INTEGER = 18_446_744_073_709_551_616 # 2^64 + BYTE_HEADERS = Array.new(256) { |byte| [byte].pack('C').freeze }.freeze def initialize @buffer = String.new @@ -173,7 +174,7 @@ def head(major_type, value) @buffer << case value when 0...24 - [major_type + value].pack('C') # 8-bit unsigned + BYTE_HEADERS[major_type + value] when 0...256 [major_type + 24, value].pack('CC') when 0...65_536 From 9e0477c98d0f4f2ea3cb93109e10c42fdf2e731c Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 21 Sep 2026 14:15:05 -0700 Subject: [PATCH 11/36] perf: reduce JSON builder allocations --- gems/smithy-json/lib/smithy-json/builder.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gems/smithy-json/lib/smithy-json/builder.rb b/gems/smithy-json/lib/smithy-json/builder.rb index 0f7bb8582..57d23ab0a 100644 --- a/gems/smithy-json/lib/smithy-json/builder.rb +++ b/gems/smithy-json/lib/smithy-json/builder.rb @@ -61,22 +61,26 @@ def map(shape, values) return if values.nil? value_member = shape.target.value - values.each.with_object({}) do |(key, value), data| + data = {} + values.each do |key, value| data[key] = build_shape(value_member, value) end + data end def structure(shape, values) return if values.nil? index = @extension.member_index(shape.target) - values.each_pair.with_object({}) do |(member_name, value), data| + data = {} + values.each_pair do |member_name, value| next if value.nil? next unless (entry = index[member_name]) wire_name, member_shape = entry data[wire_name] = build_shape(member_shape, value) end + data end def timestamp(shape, value) From 9c24f214e7a0e77f2a74e9f86f9c1a9e1a6a189b Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 21 Sep 2026 15:51:20 -0700 Subject: [PATCH 12/36] perf: avoid mutating unknown JSON unions --- gems/smithy-json/lib/smithy-json/parser.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/gems/smithy-json/lib/smithy-json/parser.rb b/gems/smithy-json/lib/smithy-json/parser.rb index 22ec9fc87..4d2230118 100644 --- a/gems/smithy-json/lib/smithy-json/parser.rb +++ b/gems/smithy-json/lib/smithy-json/parser.rb @@ -115,9 +115,14 @@ def union(shape, values, result = nil) return result.new(member_name => parse_shape(member_shape, value)) end - values.delete('__type') - key, value = values.first - target.member_type(:unknown).new(unknown: { key => value }) + unknown_union(target, values) + end + + def unknown_union(target, values) + values.each do |key, value| + return target.member_type(:unknown).new(unknown: { key => value }) unless key == '__type' + end + target.member_type(:unknown).new(unknown: { nil => nil }) end end end From ec2013aa196efdb64ad1de376ab56e9e9739a8d4 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 21 Sep 2026 16:28:43 -0700 Subject: [PATCH 13/36] perf: optimize client request preparation --- .../lib/smithy-client/endpoint_rules.rb | 2 +- .../lib/smithy-client/param_converter.rb | 8 +++-- .../lib/smithy-client/plugins/host_prefix.rb | 20 ++++++------ .../lib/smithy-schema/extension.rb | 32 ++++++++++++++++++- .../sig/smithy-schema/extension.rbs | 1 + 5 files changed, 48 insertions(+), 15 deletions(-) diff --git a/gems/smithy-client/lib/smithy-client/endpoint_rules.rb b/gems/smithy-client/lib/smithy-client/endpoint_rules.rb index 9800b002b..9dc16049c 100644 --- a/gems/smithy-client/lib/smithy-client/endpoint_rules.rb +++ b/gems/smithy-client/lib/smithy-client/endpoint_rules.rb @@ -80,7 +80,7 @@ def self.substring(input, start, stop, reverse) # Performs RFC 3986#section-2.1 defined percent-encoding on the input value. # @api private def self.uri_encode(value) - CGI.escape(value.encode('UTF-8')).gsub('+', '%20').gsub('%7E', '~') + CGI.escapeURIComponent(value.encode('UTF-8')) end # isSet(value: Option) bool diff --git a/gems/smithy-client/lib/smithy-client/param_converter.rb b/gems/smithy-client/lib/smithy-client/param_converter.rb index dd60f97a3..baa7963e2 100644 --- a/gems/smithy-client/lib/smithy-client/param_converter.rb +++ b/gems/smithy-client/lib/smithy-client/param_converter.rb @@ -63,9 +63,11 @@ def map(shape, values) key_member = shape.target.key value_member = shape.target.value - values.each.with_object({}) do |(key, value), hash| + hash = {} + values.each do |key, value| hash[convert_shape(key_member, key)] = convert_shape(value_member, value) end + hash end def structure(shape, values) @@ -220,7 +222,9 @@ def each_base_class(shape_class, &) add(MapShape, Hash) { |h, _| h.dup } add(MapShape, ::Struct) do |s| - s.members.each.with_object({}) { |k, h| h[k] = s[k] } + hash = {} + s.members.each { |member| hash[member] = s[member] } + hash end add(StringShape, String) diff --git a/gems/smithy-client/lib/smithy-client/plugins/host_prefix.rb b/gems/smithy-client/lib/smithy-client/plugins/host_prefix.rb index 8b3bad6a9..4ca094c2d 100644 --- a/gems/smithy-client/lib/smithy-client/plugins/host_prefix.rb +++ b/gems/smithy-client/lib/smithy-client/plugins/host_prefix.rb @@ -33,25 +33,23 @@ def add_handlers(handlers, config) # @api private class Handler < Smithy::Client::Handler def call(context) - host_prefix = Schema::Extension.endpoint_host_prefix(context.operation) - apply_host_prefix(context, host_prefix) if host_prefix + plan = Schema::Extension.endpoint_host_prefix_plan(context.operation) + apply_host_prefix(context, plan) if plan @handler.call(context) end private - # TODO: optimize this to collect all labels in one pass - def apply_host_prefix(context, host_prefix) - host_labels = Schema::Extension.host_label_index(context.operation.input) - prefix = host_prefix.gsub(/\{.+?}/) do |label| - label_value(host_labels, label.delete('{}'), context.params) + def apply_host_prefix(context, plan) + prefix = +'' + plan.each do |part| + value = part.is_a?(Symbol) ? label_value(part, context.params) : part + prefix << value end - context.http_request.endpoint.host = prefix + context.http_request.endpoint.host + context.http_request.endpoint.host = prefix << context.http_request.endpoint.host end - def label_value(host_labels, label, params) - name = host_labels[label] - raise ArgumentError, "#{label} is not a valid host label" if name.nil? + def label_value(name, params) raise ArgumentError, "params[:#{name}] must not be nil or blank" if params[name].nil? || params[name].empty? params[name] diff --git a/gems/smithy-schema/lib/smithy-schema/extension.rb b/gems/smithy-schema/lib/smithy-schema/extension.rb index 603ddc8b6..1bdfda48c 100644 --- a/gems/smithy-schema/lib/smithy-schema/extension.rb +++ b/gems/smithy-schema/lib/smithy-schema/extension.rb @@ -70,6 +70,10 @@ def endpoint_host_prefix(operation) (operation[KEY] || build_and_cache(operation))[:endpoint_host_prefix] end + def endpoint_host_prefix_plan(operation) + (operation[KEY] || build_and_cache(operation))[:endpoint_host_prefix_plan] + end + def request_compression_encodings(operation) (operation[KEY] || build_and_cache(operation))[:request_compression_encodings] end @@ -167,8 +171,10 @@ def build_and_cache(shape) def build_operation_metadata(operation) traits = operation.traits + endpoint_host_prefix = traits.dig('smithy.api#endpoint', 'hostPrefix') { - endpoint_host_prefix: traits.dig('smithy.api#endpoint', 'hostPrefix'), + endpoint_host_prefix: endpoint_host_prefix, + endpoint_host_prefix_plan: build_endpoint_host_prefix_plan(operation, endpoint_host_prefix), 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, @@ -177,6 +183,30 @@ def build_operation_metadata(operation) }.compact.freeze end + def build_endpoint_host_prefix_plan(operation, host_prefix) + return unless host_prefix + + host_labels = host_label_index(operation.input) + plan = [] + offset = 0 + host_prefix.to_enum(:scan, /\{(.+?)}/).each do + match = Regexp.last_match + offset = append_host_prefix_match(plan, host_prefix, host_labels, match, offset) + end + plan << host_prefix[offset..].freeze if offset < host_prefix.length + plan.freeze + end + + def append_host_prefix_match(plan, host_prefix, host_labels, match, offset) + plan << host_prefix[offset...match.begin(0)].freeze if match.begin(0) > offset + label = match[1] + name = host_labels[label] + raise ArgumentError, "#{label} is not a valid host label" unless name + + plan << name + match.end(0) + end + def build_error_index(operation) operation.errors.each_with_object({}) do |error, index| index[error.target.name] = error if error.target&.name diff --git a/gems/smithy-schema/sig/smithy-schema/extension.rbs b/gems/smithy-schema/sig/smithy-schema/extension.rbs index 020cef1a4..c86f1a8d9 100644 --- a/gems/smithy-schema/sig/smithy-schema/extension.rbs +++ b/gems/smithy-schema/sig/smithy-schema/extension.rbs @@ -9,6 +9,7 @@ module Smithy 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.endpoint_host_prefix_plan: (Shapes::OperationShape operation) -> Array[String | Symbol]? 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? From b972e210a27669840fb240ca11384e45d6be251b Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Tue, 22 Sep 2026 17:37:17 -0700 Subject: [PATCH 14/36] perf: reduce client request allocations --- gems/smithy-client/lib/smithy-client/default_params.rb | 5 +++-- gems/smithy-client/lib/smithy-client/endpoint_rules.rb | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/gems/smithy-client/lib/smithy-client/default_params.rb b/gems/smithy-client/lib/smithy-client/default_params.rb index 189d3c121..21bb9405e 100644 --- a/gems/smithy-client/lib/smithy-client/default_params.rb +++ b/gems/smithy-client/lib/smithy-client/default_params.rb @@ -53,8 +53,9 @@ def structure(shape, values) shape.target.members.each do |member_name, member_shape| value = values[member_name] - value ||= default(member_shape) if default?(shape, member_shape.traits) - next if value.nil? && !default?(shape, member_shape.traits) # default can have nil values + has_default = default?(shape, member_shape.traits) + value ||= default(member_shape) if has_default + next if value.nil? && !has_default # default can have nil values values[member_name] = apply_shape(member_shape, value) end diff --git a/gems/smithy-client/lib/smithy-client/endpoint_rules.rb b/gems/smithy-client/lib/smithy-client/endpoint_rules.rb index 9dc16049c..bf826e782 100644 --- a/gems/smithy-client/lib/smithy-client/endpoint_rules.rb +++ b/gems/smithy-client/lib/smithy-client/endpoint_rules.rb @@ -68,7 +68,7 @@ def self.parse_url(value) def self.substring(input, start, stop, reverse) return nil if start >= stop || input.size < stop - return nil if input.chars.any? { |c| c.ord > 127 } + return nil if input.each_byte.any? { |byte| byte > 127 } return input[start...stop] unless reverse From c0748e24b5771a1658a334593a9db5cffdbdcc88 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Tue, 22 Sep 2026 17:37:23 -0700 Subject: [PATCH 15/36] perf: optimize XML serialization and timestamps --- gems/smithy-xml/lib/smithy-xml/builder.rb | 4 +++- .../smithy-xml/lib/smithy-xml/parser/frame.rb | 20 +++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/gems/smithy-xml/lib/smithy-xml/builder.rb b/gems/smithy-xml/lib/smithy-xml/builder.rb index af46a357d..7bd80618d 100644 --- a/gems/smithy-xml/lib/smithy-xml/builder.rb +++ b/gems/smithy-xml/lib/smithy-xml/builder.rb @@ -99,12 +99,14 @@ def structure(name, shape, values) def structure_attrs(shape, values) attribute_members = Extension.attribute_members(shape.target) - attribute_members.each_with_object({}) do |(name, xml_name, _member_shape), attrs| + attrs = {} + attribute_members.each do |name, xml_name, _member_shape| value = values[name] next if value.nil? && !values.key?(name) attrs[xml_name] = value end + attrs end def timestamp(shape, value) diff --git a/gems/smithy-xml/lib/smithy-xml/parser/frame.rb b/gems/smithy-xml/lib/smithy-xml/parser/frame.rb index 3c3b5ec84..111c4e6d5 100644 --- a/gems/smithy-xml/lib/smithy-xml/parser/frame.rb +++ b/gems/smithy-xml/lib/smithy-xml/parser/frame.rb @@ -262,6 +262,8 @@ def consume_child_frame(child) # rubocop:disable Metrics/AbcSize, Metrics/Cyclom # @api private class TimestampFrame < Frame + NUMERIC_TIMESTAMP = /^[\d.]+$/ + def result @text.empty? ? nil : deserialize_time(@text.join) end @@ -269,16 +271,14 @@ def result # @param [String] value # @return [Time] def deserialize_time(value) - case value - when nil then nil - when /^[\d.]+$/ then Time.at(value.to_f).utc - else - begin - fractional_time = Time.parse(value).to_f - Time.at(fractional_time).utc - rescue ArgumentError - raise "unhandled timestamp format `#{value}'" - end + return if value.nil? + return Time.at(value.to_f).utc if NUMERIC_TIMESTAMP.match?(value) + + begin + fractional_time = Time.parse(value).to_f + Time.at(fractional_time).utc + rescue ArgumentError + raise "unhandled timestamp format `#{value}'" end end end From 756702ced0124e6619414761eac5c4c074e79adf Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Tue, 22 Sep 2026 17:37:29 -0700 Subject: [PATCH 16/36] perf: reduce CBOR builder allocations --- gems/smithy-cbor/lib/smithy-cbor/builder.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gems/smithy-cbor/lib/smithy-cbor/builder.rb b/gems/smithy-cbor/lib/smithy-cbor/builder.rb index 64a3e7b65..8bd951417 100644 --- a/gems/smithy-cbor/lib/smithy-cbor/builder.rb +++ b/gems/smithy-cbor/lib/smithy-cbor/builder.rb @@ -45,21 +45,25 @@ def map(shape, values) return if values.nil? value_member = shape.target.value - values.each.with_object({}) do |(key, value), data| + data = {} + values.each do |key, value| data[key] = build_shape(value_member, value) end + data end def structure(shape, values) return if values.nil? target = shape.target - values.each_pair.with_object({}) do |(member_name, value), data| + data = {} + values.each_pair do |member_name, value| next if value.nil? next unless (member_shape = target.member(member_name)) data[member_shape.name] = build_shape(member_shape, value) end + data end def union(shape, values) From d9384c4b95ccb5eafcb39cccee6d19bbb5f176af Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Tue, 22 Sep 2026 17:37:35 -0700 Subject: [PATCH 17/36] perf: optimize document conversion --- .../document_utils/deserializer.rb | 41 +++++++++++-------- .../document_utils/serializer.rb | 17 +++++--- .../lib/smithy-schema/structure.rb | 6 ++- 3 files changed, 39 insertions(+), 25 deletions(-) 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 01872ea5d..ad1d6dbbe 100644 --- a/gems/smithy-schema/lib/smithy-schema/document_utils/deserializer.rb +++ b/gems/smithy-schema/lib/smithy-schema/document_utils/deserializer.rb @@ -8,6 +8,8 @@ module DocumentUtils class Deserializer include Shapes + NUMERIC_TIMESTAMP = /^[\d.]+$/ + def initialize(options = {}) @type_registry = options[:type_registry] end @@ -54,9 +56,10 @@ def float(value) def list(shape, values, result = nil) return if values.nil? + member = shape.target.member result = [] if result.nil? values.each do |value| - result << deserialize_shape(shape.target.member, value) unless value.nil? + result << deserialize_shape(member, value) unless value.nil? end result end @@ -64,9 +67,10 @@ def list(shape, values, result = nil) def map(shape, values, result = nil) return if values.nil? + value_member = shape.target.value result = {} if result.nil? values.each do |key, value| - result[key] = deserialize_shape(shape.target.value, value) unless value.nil? + result[key] = deserialize_shape(value_member, value) unless value.nil? end result end @@ -84,19 +88,15 @@ def structure(shape, values, result = nil) end def timestamp(value) - case value - when nil then nil - when Numeric - Time.at(value).utc - when /^[\d.]+$/ - Time.at(value.to_f).utc - else - begin - fractional_time = Time.parse(value).to_f - Time.at(fractional_time).utc - rescue ArgumentError - raise "unhandled timestamp format `#{value}'" - end + return if value.nil? + return Time.at(value).utc if value.is_a?(Numeric) + return Time.at(value.to_f).utc if NUMERIC_TIMESTAMP.match?(value) + + begin + fractional_time = Time.parse(value).to_f + Time.at(fractional_time).utc + rescue ArgumentError + raise "unhandled timestamp format `#{value}'" end end @@ -110,9 +110,14 @@ def union(shape, values, result = nil) return result.new(member_name => deserialize_shape(member_shape, value)) end - values.delete('__type') - key, value = values.first - target.member_type(:unknown).new(key, value) + unknown_union(target, values) + end + + def unknown_union(target, values) + values.each do |key, value| + return target.member_type(:unknown).new(key, value) unless key == '__type' + end + target.member_type(:unknown).new(nil, nil) end end end diff --git a/gems/smithy-schema/lib/smithy-schema/document_utils/serializer.rb b/gems/smithy-schema/lib/smithy-schema/document_utils/serializer.rb index bc169801b..249d880be 100644 --- a/gems/smithy-schema/lib/smithy-schema/document_utils/serializer.rb +++ b/gems/smithy-schema/lib/smithy-schema/document_utils/serializer.rb @@ -30,9 +30,11 @@ def serialize_untyped(values) case values when Time then values.utc.to_i # timestamp format is "epoch-seconds" by default when Hash - values.each_with_object({}) do |(k, v), h| - h[k.to_s] = serialize_untyped(v) + data = {} + values.each do |k, v| + data[k.to_s] = serialize_untyped(v) end + data when Array then values.map { |d| serialize_untyped(d) } else values end @@ -101,18 +103,23 @@ def map(shape, values) return if values.nil? value_shape = shape.target.value - values.each.with_object({}) do |(key, value), data| + data = {} + values.each do |key, value| data[key.to_s] = serialize_shape(value_shape, value) end + data end def structure(shape, values) return if values.nil? - shape.target.members.each_with_object({}) do |(member_name, member_shape), data| - value = resolve_value(member_name, member_shape, values.to_h) + values = values.to_h + data = {} + shape.target.members.each do |member_name, member_shape| + value = resolve_value(member_name, member_shape, values) data[wire_name(member_shape)] = serialize_shape(member_shape, value) unless value.nil? end + data end def timestamp(shape, value) diff --git a/gems/smithy-schema/lib/smithy-schema/structure.rb b/gems/smithy-schema/lib/smithy-schema/structure.rb index 7683b374c..3275dcc83 100644 --- a/gems/smithy-schema/lib/smithy-schema/structure.rb +++ b/gems/smithy-schema/lib/smithy-schema/structure.rb @@ -41,10 +41,12 @@ def key?(member_name) private def _to_h_structure(obj) - obj.members.each_with_object({}) do |member, hash| - value = obj.send(member) + hash = {} + obj.members.each do |member| + value = obj[member] hash[member] = to_hash(value) unless value.nil? end + hash end def _to_h_hash(obj) From f3c373e4e9222139561fe911e69788f9b0db4c57 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Tue, 22 Sep 2026 18:54:46 -0700 Subject: [PATCH 18/36] perf: optimize host label validation --- gems/smithy-client/lib/smithy-client/endpoint_rules.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gems/smithy-client/lib/smithy-client/endpoint_rules.rb b/gems/smithy-client/lib/smithy-client/endpoint_rules.rb index bf826e782..7b6f3bf1a 100644 --- a/gems/smithy-client/lib/smithy-client/endpoint_rules.rb +++ b/gems/smithy-client/lib/smithy-client/endpoint_rules.rb @@ -11,6 +11,8 @@ module Client # The rules engine has a set of included functions that can be # invoked without additional dependencies, called the standard library. module EndpointRules + HOST_LABEL = /\A(?!-)[a-zA-Z0-9-]{1,63}(? Date: Tue, 22 Sep 2026 18:54:52 -0700 Subject: [PATCH 19/36] perf: optimize CBOR bignum and union parsing --- gems/smithy-cbor/lib/smithy-cbor/decoder.rb | 7 ++++--- gems/smithy-cbor/lib/smithy-cbor/parser.rb | 11 ++++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/gems/smithy-cbor/lib/smithy-cbor/decoder.rb b/gems/smithy-cbor/lib/smithy-cbor/decoder.rb index 952b4d461..017a2cc2e 100644 --- a/gems/smithy-cbor/lib/smithy-cbor/decoder.rb +++ b/gems/smithy-cbor/lib/smithy-cbor/decoder.rb @@ -155,9 +155,10 @@ def read_big_decimal def read_bignum(tag_value) add_info = read_byte & FIVE_BIT_MASK bstr = take(read_count(add_info)) - v = bstr.bytes.inject(0) do |sum, b| - sum <<= 8 - sum + b + v = 0 + bstr.each_byte do |b| + v <<= 8 + v += b end case tag_value when 2 then v diff --git a/gems/smithy-cbor/lib/smithy-cbor/parser.rb b/gems/smithy-cbor/lib/smithy-cbor/parser.rb index 34dbb3e11..d59889e6a 100644 --- a/gems/smithy-cbor/lib/smithy-cbor/parser.rb +++ b/gems/smithy-cbor/lib/smithy-cbor/parser.rb @@ -85,9 +85,14 @@ def union(shape, values, result = nil) return result.new(member_name => parse_shape(member_shape, value)) end - values.delete('__type') - key, value = values.first - target.member_type(:unknown).new(unknown: { key => value }) + unknown_union(target, values) + end + + def unknown_union(target, values) + values.each do |key, value| + return target.member_type(:unknown).new(unknown: { key => value }) unless key == '__type' + end + target.member_type(:unknown).new(unknown: { nil => nil }) end end end From 66931cdb824f19b8017c3f1aca03327f5ed6b95d Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Tue, 22 Sep 2026 18:54:59 -0700 Subject: [PATCH 20/36] perf: reduce XML attribute rendering allocations --- gems/smithy-xml/lib/smithy-xml/doc_builder.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gems/smithy-xml/lib/smithy-xml/doc_builder.rb b/gems/smithy-xml/lib/smithy-xml/doc_builder.rb index e527a65db..6435ef846 100644 --- a/gems/smithy-xml/lib/smithy-xml/doc_builder.rb +++ b/gems/smithy-xml/lib/smithy-xml/doc_builder.rb @@ -66,7 +66,11 @@ def close_el(name) def attributes(attr) return '' if attr.empty? - " #{attr.map { |key, value| "#{key}=#{escape(value.to_s, :attr)}" }.join(' ')}" + output = String.new + attr.each do |key, value| + output << ' ' << key.to_s << '=' << escape(value.to_s, :attr) + end + output end def escape(string, text_or_attr) From 7f9db9b0c4c0c9acb22be50d2729b25b8da9a787 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Tue, 22 Sep 2026 20:21:13 -0700 Subject: [PATCH 21/36] perf: prioritize common CBOR encoder types --- gems/smithy-cbor/lib/smithy-cbor/encoder.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gems/smithy-cbor/lib/smithy-cbor/encoder.rb b/gems/smithy-cbor/lib/smithy-cbor/encoder.rb index 58dcb6931..e6b7ee56c 100644 --- a/gems/smithy-cbor/lib/smithy-cbor/encoder.rb +++ b/gems/smithy-cbor/lib/smithy-cbor/encoder.rb @@ -38,17 +38,17 @@ def bytes # generic method for adding generic Ruby data based on its type def add(value) # rubocop:disable Metrics case value - when BigDecimal then add_big_decimal(value) + when String then process_string(value) when Integer then add_auto_integer(value) + when BigDecimal then add_big_decimal(value) when Numeric then add_auto_float(value) - when Symbol then add_string(value.to_s) when true, false then add_boolean(value) when nil then add_nil - when Tagged then process_tag(value) - when String then process_string(value) - when Array then add_array(value) when Hash then add_hash(value) + when Array then add_array(value) + when Symbol then add_string(value.to_s) when Time then add_time(value) + when Tagged then process_tag(value) else raise BuildError, "Unable to encode #{value}" end self From 4dd3a356d6487815358a49327889e63c62785844 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Tue, 22 Sep 2026 20:21:24 -0700 Subject: [PATCH 22/36] perf: optimize fixed response status checks --- .../smithy-client/lib/smithy-client/http/error_inspector.rb | 6 ++++-- gems/smithy-client/lib/smithy-client/rpc_v2_cbor.rb | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/gems/smithy-client/lib/smithy-client/http/error_inspector.rb b/gems/smithy-client/lib/smithy-client/http/error_inspector.rb index feeeb7ffc..924f51679 100644 --- a/gems/smithy-client/lib/smithy-client/http/error_inspector.rb +++ b/gems/smithy-client/lib/smithy-client/http/error_inspector.rb @@ -54,11 +54,13 @@ def throttling? end def server? - (500..599).cover?(@http_response.status_code) + status_code = @http_response.status_code + status_code >= 500 && status_code <= 599 # rubocop:disable Style/ComparableBetween end def client? - (400..499).cover?(@http_response.status_code) + status_code = @http_response.status_code + status_code >= 400 && status_code <= 499 # rubocop:disable Style/ComparableBetween end def modeled_retryable? diff --git a/gems/smithy-client/lib/smithy-client/rpc_v2_cbor.rb b/gems/smithy-client/lib/smithy-client/rpc_v2_cbor.rb index 343f5910a..70076c5dc 100644 --- a/gems/smithy-client/lib/smithy-client/rpc_v2_cbor.rb +++ b/gems/smithy-client/lib/smithy-client/rpc_v2_cbor.rb @@ -32,7 +32,8 @@ def parse_data(context) # @param [HandlerContext] context # @return [StandardError, nil] def parse_error(context) - return unless (200..599).cover?(context.http_response.status_code) + status_code = context.http_response.status_code + return unless status_code >= 200 && status_code <= 599 # rubocop:disable Style/ComparableBetween # Malformed responses should raise an http-based error, so we validate # the protocol header across the full 200..599 range. @@ -40,7 +41,7 @@ def parse_error(context) code, data = http_status_error(context) return build_error(context, code, data) end - return unless (400..599).cover?(context.http_response.status_code) + return unless status_code >= 400 && status_code <= 599 # rubocop:disable Style/ComparableBetween error(context) end From c3ed61897476550a0e1cc9e457014354bfd918ac Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Tue, 22 Sep 2026 20:21:56 -0700 Subject: [PATCH 23/36] Revert "perf: reduce XML attribute rendering allocations" This reverts commit 66931cdb824f19b8017c3f1aca03327f5ed6b95d. --- gems/smithy-xml/lib/smithy-xml/doc_builder.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/gems/smithy-xml/lib/smithy-xml/doc_builder.rb b/gems/smithy-xml/lib/smithy-xml/doc_builder.rb index 6435ef846..e527a65db 100644 --- a/gems/smithy-xml/lib/smithy-xml/doc_builder.rb +++ b/gems/smithy-xml/lib/smithy-xml/doc_builder.rb @@ -66,11 +66,7 @@ def close_el(name) def attributes(attr) return '' if attr.empty? - output = String.new - attr.each do |key, value| - output << ' ' << key.to_s << '=' << escape(value.to_s, :attr) - end - output + " #{attr.map { |key, value| "#{key}=#{escape(value.to_s, :attr)}" }.join(' ')}" end def escape(string, text_or_attr) From 8359e7e87eefe7467d6a8b76997deb45b7bc46da Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Tue, 22 Sep 2026 21:29:36 -0700 Subject: [PATCH 24/36] perf: flatten XML extension metadata --- .../lib/smithy-schema/extension.rb | 4 +- .../smithy-schema/lib/smithy-schema/shapes.rb | 22 +++ .../sig/smithy-schema/shapes.rbs | 2 + .../spec/smithy-schema/shapes_spec.rb | 35 +++++ gems/smithy-xml/lib/smithy-xml/extension.rb | 148 +++++++----------- .../spec/smithy-xml/extension_spec.rb | 22 +-- 6 files changed, 124 insertions(+), 109 deletions(-) diff --git a/gems/smithy-schema/lib/smithy-schema/extension.rb b/gems/smithy-schema/lib/smithy-schema/extension.rb index 1bdfda48c..9fbafc655 100644 --- a/gems/smithy-schema/lib/smithy-schema/extension.rb +++ b/gems/smithy-schema/lib/smithy-schema/extension.rb @@ -150,7 +150,9 @@ def each_member(shape, &block) # Extension.sparse?(list) # # => true def sparse?(shape) - shape.traits.key?('smithy.api#sparse') + shape.fetch_metadata(:sparse) do + shape.traits.key?('smithy.api#sparse') + end end private diff --git a/gems/smithy-schema/lib/smithy-schema/shapes.rb b/gems/smithy-schema/lib/smithy-schema/shapes.rb index ece5abeee..7eb231722 100644 --- a/gems/smithy-schema/lib/smithy-schema/shapes.rb +++ b/gems/smithy-schema/lib/smithy-schema/shapes.rb @@ -38,6 +38,17 @@ def key?(key) @metadata.key?(key) end + # Fetches a cached metadata value, resolving and storing it when absent. + # Unlike +||=+, this preserves cached +nil+ and +false+ values. + # + # @param [Symbol] key + # @return [Object] + def fetch_metadata(key) + @metadata.fetch(key) do + @metadata[key] = yield + end + end + # @param [Symbol] key # @param [Object] value def []=(key, value) @@ -74,6 +85,17 @@ def key?(key) @metadata.key?(key) end + # Fetches a cached metadata value, resolving and storing it when absent. + # Unlike +||=+, this preserves cached +nil+ and +false+ values. + # + # @param [Symbol] key + # @return [Object] + def fetch_metadata(key) + @metadata.fetch(key) do + @metadata[key] = yield + end + end + # @param [Symbol] key # @param [Object] value def []=(key, value) diff --git a/gems/smithy-schema/sig/smithy-schema/shapes.rbs b/gems/smithy-schema/sig/smithy-schema/shapes.rbs index 6760a72cc..86e209510 100644 --- a/gems/smithy-schema/sig/smithy-schema/shapes.rbs +++ b/gems/smithy-schema/sig/smithy-schema/shapes.rbs @@ -10,6 +10,7 @@ module Smithy attr_accessor traits: Hash[String, untyped] def []: (Symbol) -> Object def key?: (Symbol) -> bool + def fetch_metadata: (Symbol) { () -> Object } -> Object def []=: (Symbol, Object) -> void end @@ -21,6 +22,7 @@ module Smithy attr_accessor traits: Hash[String, untyped] def []: (Symbol) -> Object def key?: (Symbol) -> bool + def fetch_metadata: (Symbol) { () -> Object } -> Object def []=: (Symbol, Object) -> void end diff --git a/gems/smithy-schema/spec/smithy-schema/shapes_spec.rb b/gems/smithy-schema/spec/smithy-schema/shapes_spec.rb index 0c74c5a25..4050fea5c 100644 --- a/gems/smithy-schema/spec/smithy-schema/shapes_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/shapes_spec.rb @@ -37,6 +37,34 @@ module Shapes subject[:foo] = 'bar' expect(subject[:foo]).to eq('bar') end + + it 'fetches metadata once when the resolved value is false' do + calls = 0 + + 2.times do + subject.fetch_metadata(:foo) do + calls += 1 + false + end + end + + expect(calls).to eq(1) + expect(subject[:foo]).to be(false) + end + + it 'fetches metadata once when the resolved value is nil' do + calls = 0 + + 2.times do + subject.fetch_metadata(:foo) do + calls += 1 + nil + end + end + + expect(calls).to eq(1) + expect(subject).to be_key(:foo) + end end end @@ -67,6 +95,13 @@ module Shapes subject[:foo] = 'bar' expect(subject[:foo]).to eq('bar') end + + it 'fetches and memoizes metadata' do + value = Object.new + + expect(subject.fetch_metadata(:foo) { value }).to be(value) + expect(subject.fetch_metadata(:foo) { raise 'resolved twice' }).to be(value) + end end describe ServiceShape do diff --git a/gems/smithy-xml/lib/smithy-xml/extension.rb b/gems/smithy-xml/lib/smithy-xml/extension.rb index 1e9f845fc..7c23d9a31 100644 --- a/gems/smithy-xml/lib/smithy-xml/extension.rb +++ b/gems/smithy-xml/lib/smithy-xml/extension.rb @@ -5,86 +5,66 @@ module Xml # XML-specific lookup helpers and cached serde metadata. # # Raw Smithy trait data remains on +shape.traits+ and +member.traits+ with - # string keys. This extension caches XML-specific values under - # +object[KEY]+; generic target metadata remains owned by - # +Schema::Extension+. + # string keys. Resolved XML values are cached as flat, XML-prefixed keys + # on their owning shape or member. # @api private module Extension - KEY = :xml - class << self - # Returns cached XML metadata for a shape or member. - # - # Example: - # Extension.fetch(member) - # # => { xml_wire_name: 'Item', ... } - def fetch(shape) - shape[KEY] || build_and_cache(shape) - end - # Returns the XML wrapper or structure name. - # - # Example: - # Extension.structure_name(shape) - # # => 'Result' def structure_name(shape) - (shape[KEY] || build_and_cache(shape))[:xml_structure_name] + shape.fetch_metadata(:xml_structure_name) do + resolve_structure_name(shape) + end end # Preserves the existing true-or-nil return contract. - # - # Example: - # Extension.flattened?(member) - # # => true def flattened?(shape) - shape.traits.key?('smithy.api#xmlFlattened') || nil + shape.fetch_metadata(:xml_flattened) do + shape.traits.key?('smithy.api#xmlFlattened') || nil + end end # Returns the parser frame class for the shape. - # - # Example: - # Extension.frame_class(shape) - # # => Parser::ListFrame def frame_class(shape) - (shape[KEY] || build_and_cache(shape))[:xml_frame_class] + shape.fetch_metadata(:xml_frame_class) do + frame_class_for(shape.target, flattened?(shape)) + end end # Returns the resolved XML member name. - # - # Example: - # Extension.wire_name(member) - # # => 'Item' def wire_name(member) - (member[KEY] || build_and_cache(member))[:xml_wire_name] + member[:xml_wire_name] ||= member.traits['smithy.api#xmlName'] || member.name end # Returns XML members partitioned into attributes and elements. - # - # Example: - # Extension.members(shape) - # # => { attributes: [...], elements: [...] } def members(shape) - (shape[KEY] || build_and_cache(shape))[:xml_members] + resolve_members(shape) + shape[:xml_members] end def attribute_members(shape) - members(shape)[:attributes] + resolve_members(shape) + shape[:xml_attribute_members] end def element_members(shape) - members(shape)[:elements] + resolve_members(shape) + shape[:xml_element_members] end def member_index(shape) - (shape[KEY] || build_and_cache(shape))[:xml_member_index] + resolve_members(shape) + shape[:xml_member_index] end def namespace_attrs(shape) - (shape[KEY] || build_and_cache(shape))[:xml_namespace_attrs] + shape[:xml_namespace_attrs] ||= build_namespace_attrs(shape, shape.target) end def map_parts(shape) - (shape[KEY] || build_and_cache(shape))[:xml_map_parts] + shape.fetch_metadata(:xml_map_parts) do + build_map_parts(shape.target) + end end def timestamp_format(shape) @@ -97,71 +77,55 @@ def sparse?(shape) private - def build_and_cache(shape) - Schema::Extension.fetch(shape) - shape[KEY] = - if shape.is_a?(Schema::Shapes::MemberShape) - build_member_metadata(shape) - else - build_shape_metadata(shape) - end + def resolve_structure_name(shape) # rubocop:disable Metrics/CyclomaticComplexity + target = shape.target + return shape.traits['smithy.api#xmlName'] || target.name unless shape.is_a?(Schema::Shapes::MemberShape) + + xml_name = shape.traits['smithy.api#xmlName'] + structure_name = xml_name || target.traits['smithy.api#xmlName'] + if structure_name.nil? && + (target.is_a?(Schema::Shapes::StructureShape) || target.is_a?(Schema::Shapes::UnionShape)) + structure_name = target.name + end + structure_name || shape.name end - def build_shape_metadata(shape) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength - target = shape.target - metadata = { - xml_structure_name: shape.traits['smithy.api#xmlName'] || target.name, - xml_namespace_attrs: build_namespace_attrs(shape, target), - xml_frame_class: frame_class_for(target, flattened?(shape)) - } - if target.is_a?(Schema::Shapes::StructureShape) || target.is_a?(Schema::Shapes::UnionShape) - members = { attributes: [], elements: [] } + def resolve_members(shape) # rubocop:disable Metrics/AbcSize + shape.fetch_metadata(:xml_members_resolved) do + attributes = [] + elements = [] index = {} Schema::Extension.each_member(shape) do |ruby_name, member| - member_metadata = fetch(member) - xml_name = member_metadata[:xml_wire_name] + xml_name = wire_name(member) entry = [ruby_name, xml_name, member].freeze index[xml_name] = [ruby_name, member].freeze - members[member_metadata[:xml_attribute] ? :attributes : :elements] << entry + (attribute?(member) ? attributes : elements) << entry end - metadata[:xml_members] = { - attributes: members[:attributes].freeze, - elements: members[:elements].freeze - }.freeze - metadata[:xml_member_index] = index.freeze - else - add_map_parts(metadata, target) + + attributes.freeze + elements.freeze + shape[:xml_attribute_members] = attributes + shape[:xml_element_members] = elements + shape[:xml_members] = { attributes: attributes, elements: elements }.freeze + shape[:xml_member_index] = index.freeze + true end - metadata.freeze end - def build_member_metadata(member) # rubocop:disable Metrics/AbcSize - target = member.target - xml_name = member.traits['smithy.api#xmlName'] - structure_name = xml_name || target.traits['smithy.api#xmlName'] - if structure_name.nil? && - (target.is_a?(Schema::Shapes::StructureShape) || target.is_a?(Schema::Shapes::UnionShape)) - structure_name = target.name + def attribute?(member) + member.fetch_metadata(:xml_attribute) do + member.traits.key?('smithy.api#xmlAttribute') end - metadata = { - xml_structure_name: structure_name || member.name, - xml_wire_name: xml_name || member.name, - xml_namespace_attrs: build_namespace_attrs(member, target), - xml_attribute: member.traits.key?('smithy.api#xmlAttribute'), - xml_frame_class: frame_class_for(target, flattened?(member)) - } - add_map_parts(metadata, target) - metadata.freeze - end - - def add_map_parts(metadata, target) + end + + def build_map_parts(target) return unless target.is_a?(Schema::Shapes::MapShape) key_member = target.key value_member = target.value return unless key_member && value_member - metadata[:xml_map_parts] = [ + [ wire_name(key_member), key_member, wire_name(value_member), value_member ].freeze end diff --git a/gems/smithy-xml/spec/smithy-xml/extension_spec.rb b/gems/smithy-xml/spec/smithy-xml/extension_spec.rb index 7af9a2203..8807411f2 100644 --- a/gems/smithy-xml/spec/smithy-xml/extension_spec.rb +++ b/gems/smithy-xml/spec/smithy-xml/extension_spec.rb @@ -28,12 +28,12 @@ module Xml ) expect(described_class.structure_name(member)).to eq('RootElement') - expect(member[:xml][:xml_structure_name]).to eq('RootElement') + expect(member[:xml_structure_name]).to eq('RootElement') end it 'falls back to the target structure name' do expect(described_class.structure_name(structure)).to eq('Structure') - expect(structure[:xml][:xml_structure_name]).to eq('Structure') + expect(structure[:xml_structure_name]).to eq('Structure') end it 'memoizes the structure element name on shape metadata' do @@ -41,16 +41,6 @@ module Xml end end - describe '.fetch' do - it 'populates shared schema metadata before XML metadata' do - shape = Schema::Shapes::StringShape.new - - expect(shape[:schema]).to be_nil - described_class.fetch(shape) - expect(shape[:schema]).to eq({}) - end - end - describe '.wire_name' do it 'prefers xmlName when present' do member = Schema::Shapes::MemberShape.new( @@ -60,12 +50,12 @@ module Xml ) expect(described_class.wire_name(member)).to eq('NewString') - expect(member[:xml][:xml_wire_name]).to eq('NewString') + expect(member[:xml_wire_name]).to eq('NewString') end it 'falls back to the provided default' do expect(described_class.wire_name(element_member)).to eq('String') - expect(element_member[:xml][:xml_wire_name]).to eq('String') + expect(element_member[:xml_wire_name]).to eq('String') end end @@ -97,8 +87,8 @@ module Xml 'Status' => [:status, attribute_member] ) expect(described_class.member_index(structure)).to be_frozen - expect(element_member[:xml][:xml_wire_name]).to eq('String') - expect(attribute_member[:xml][:xml_wire_name]).to eq('Status') + expect(element_member[:xml_wire_name]).to eq('String') + expect(attribute_member[:xml_wire_name]).to eq('Status') end it 'memoizes the index on the shape metadata' do From b8141749d55a908802ce4fa2d1019d560a0d68b9 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Wed, 23 Sep 2026 07:15:51 -0700 Subject: [PATCH 25/36] fix: cache XML flattened as a boolean --- gems/smithy-xml/lib/smithy-xml/extension.rb | 4 ++-- gems/smithy-xml/spec/smithy-xml/extension_spec.rb | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/gems/smithy-xml/lib/smithy-xml/extension.rb b/gems/smithy-xml/lib/smithy-xml/extension.rb index 7c23d9a31..a3bc352c0 100644 --- a/gems/smithy-xml/lib/smithy-xml/extension.rb +++ b/gems/smithy-xml/lib/smithy-xml/extension.rb @@ -17,10 +17,10 @@ def structure_name(shape) end end - # Preserves the existing true-or-nil return contract. + # Returns whether the XML value is flattened. def flattened?(shape) shape.fetch_metadata(:xml_flattened) do - shape.traits.key?('smithy.api#xmlFlattened') || nil + shape.traits.key?('smithy.api#xmlFlattened') end end diff --git a/gems/smithy-xml/spec/smithy-xml/extension_spec.rb b/gems/smithy-xml/spec/smithy-xml/extension_spec.rb index 8807411f2..1079651bf 100644 --- a/gems/smithy-xml/spec/smithy-xml/extension_spec.rb +++ b/gems/smithy-xml/spec/smithy-xml/extension_spec.rb @@ -59,6 +59,13 @@ module Xml end end + describe '.flattened?' do + it 'caches false when the trait is absent' do + expect(described_class.flattened?(element_member)).to be(false) + expect(element_member[:xml_flattened]).to be(false) + end + end + describe '.members' do it 'returns ordered element and attribute members' do structure.add_member(:string, element_member) From a11857a2b2534581c252d2857365a749cb2f387e Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Wed, 23 Sep 2026 07:29:35 -0700 Subject: [PATCH 26/36] perf: streamline XML member metadata reads --- gems/smithy-xml/lib/smithy-xml/extension.rb | 56 +++++++++++---------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/gems/smithy-xml/lib/smithy-xml/extension.rb b/gems/smithy-xml/lib/smithy-xml/extension.rb index a3bc352c0..10bf65c7c 100644 --- a/gems/smithy-xml/lib/smithy-xml/extension.rb +++ b/gems/smithy-xml/lib/smithy-xml/extension.rb @@ -38,23 +38,19 @@ def wire_name(member) # Returns XML members partitioned into attributes and elements. def members(shape) - resolve_members(shape) - shape[:xml_members] + shape[:xml_members] || resolve_members(shape, :members) end def attribute_members(shape) - resolve_members(shape) - shape[:xml_attribute_members] + shape[:xml_attribute_members] || resolve_members(shape, :attributes) end def element_members(shape) - resolve_members(shape) - shape[:xml_element_members] + shape[:xml_element_members] || resolve_members(shape, :elements) end def member_index(shape) - resolve_members(shape) - shape[:xml_member_index] + shape[:xml_member_index] || resolve_members(shape, :index) end def namespace_attrs(shape) @@ -90,25 +86,31 @@ def resolve_structure_name(shape) # rubocop:disable Metrics/CyclomaticComplexity structure_name || shape.name end - def resolve_members(shape) # rubocop:disable Metrics/AbcSize - shape.fetch_metadata(:xml_members_resolved) do - attributes = [] - elements = [] - index = {} - Schema::Extension.each_member(shape) do |ruby_name, member| - xml_name = wire_name(member) - entry = [ruby_name, xml_name, member].freeze - index[xml_name] = [ruby_name, member].freeze - (attribute?(member) ? attributes : elements) << entry - end - - attributes.freeze - elements.freeze - shape[:xml_attribute_members] = attributes - shape[:xml_element_members] = elements - shape[:xml_members] = { attributes: attributes, elements: elements }.freeze - shape[:xml_member_index] = index.freeze - true + def resolve_members(shape, result) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + attributes = [] + elements = [] + index = {} + Schema::Extension.each_member(shape) do |ruby_name, member| + xml_name = wire_name(member) + entry = [ruby_name, xml_name, member].freeze + index[xml_name] = [ruby_name, member].freeze + (attribute?(member) ? attributes : elements) << entry + end + + attributes.freeze + elements.freeze + members = { attributes: attributes, elements: elements }.freeze + index.freeze + shape[:xml_attribute_members] = attributes + shape[:xml_element_members] = elements + shape[:xml_members] = members + shape[:xml_member_index] = index + + case result + when :members then members + when :attributes then attributes + when :elements then elements + when :index then index end end From a17269e31a4d04ef4ec55a78b4d52c37f16bc1e7 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Wed, 23 Sep 2026 07:32:42 -0700 Subject: [PATCH 27/36] perf: flatten schema extension metadata --- .../lib/smithy-client/http_extension.rb | 1 - .../spec/smithy-client/http_extension_spec.rb | 5 +- gems/smithy-json/lib/smithy-json/extension.rb | 1 - .../spec/smithy-json/extension_spec.rb | 2 - .../lib/smithy-schema/extension.rb | 244 ++++++++---------- .../sig/smithy-schema/extension.rbs | 14 +- .../spec/smithy-schema/extension_spec.rb | 19 +- 7 files changed, 133 insertions(+), 153 deletions(-) diff --git a/gems/smithy-client/lib/smithy-client/http_extension.rb b/gems/smithy-client/lib/smithy-client/http_extension.rb index 96f18359a..816372465 100644 --- a/gems/smithy-client/lib/smithy-client/http_extension.rb +++ b/gems/smithy-client/lib/smithy-client/http_extension.rb @@ -117,7 +117,6 @@ def response_code_member(shape) private def build_and_cache(shape) - Schema::Extension.fetch(shape) shape[KEY] = if shape.is_a?(Schema::Shapes::OperationShape) operation_metadata(shape) diff --git a/gems/smithy-client/spec/smithy-client/http_extension_spec.rb b/gems/smithy-client/spec/smithy-client/http_extension_spec.rb index 917389df6..38ab4662d 100644 --- a/gems/smithy-client/spec/smithy-client/http_extension_spec.rb +++ b/gems/smithy-client/spec/smithy-client/http_extension_spec.rb @@ -5,12 +5,11 @@ module Smithy module Client describe HttpExtension do - it 'populates shared schema metadata before empty HTTP metadata' do + it 'caches empty HTTP metadata for unsupported shapes' do shape = Schema::Shapes::StringShape.new - expect(shape[:schema]).to be_nil expect(described_class.fetch(shape)).to eq({}) - expect(shape[:schema]).to eq({}) + expect(described_class.fetch(shape)).to be(shape[:http]) end it 'caches HTTP operation metadata' do diff --git a/gems/smithy-json/lib/smithy-json/extension.rb b/gems/smithy-json/lib/smithy-json/extension.rb index 9a42801bd..de0d5ab9b 100644 --- a/gems/smithy-json/lib/smithy-json/extension.rb +++ b/gems/smithy-json/lib/smithy-json/extension.rb @@ -73,7 +73,6 @@ def timestamp_format(shape) private def build_and_cache(shape) - Schema::Extension.fetch(shape) shape[KEY] = case shape when Schema::Shapes::StructureShape, Schema::Shapes::UnionShape diff --git a/gems/smithy-json/spec/smithy-json/extension_spec.rb b/gems/smithy-json/spec/smithy-json/extension_spec.rb index 020ea249a..d2dda4825 100644 --- a/gems/smithy-json/spec/smithy-json/extension_spec.rb +++ b/gems/smithy-json/spec/smithy-json/extension_spec.rb @@ -65,10 +65,8 @@ module Json it 'caches a truthy empty payload for unsupported shape kinds' do shape = Schema::Shapes::StringShape.new - expect(shape[:schema]).to be_nil expect(described_class.fetch(shape)).to be_empty expect(described_class.fetch(shape)).to be(shape[:json]) - expect(shape[:schema]).to eq({}) end end end diff --git a/gems/smithy-schema/lib/smithy-schema/extension.rb b/gems/smithy-schema/lib/smithy-schema/extension.rb index 9fbafc655..0be431cd7 100644 --- a/gems/smithy-schema/lib/smithy-schema/extension.rb +++ b/gems/smithy-schema/lib/smithy-schema/extension.rb @@ -5,139 +5,133 @@ module Schema # Cached schema metadata shared by Smithy protocol codecs. # # Raw Smithy trait data remains on +shape.traits+ and +member.traits+ with - # string keys. This module resolves modeled-member indexes. Protocol - # extensions own wire-specific metadata. + # string keys. Resolved values are cached as flat, schema-prefixed keys on + # their owning shape, member, or operation. # @api private module Extension - KEY = :schema - class << self - # Returns the complete cached Schema metadata payload. - # - # Example: - # Extension.fetch(shape) - # # => { wire_index: ..., ... } - def fetch(shape) - shape[KEY] || build_and_cache(shape) - end - # Returns the modeled wire-name lookup used by existing serde # consumers. The index maps modeled member name to # [ruby_member_name, member_shape]. - # - # Example: - # Extension.wire_index(shape) - # # => { 'wireName' => [:ruby_name, member] } def wire_index(shape) - (shape[KEY] || build_and_cache(shape))[:wire_index] + shape[:schema_wire_index] || resolve_aggregate(shape, :wire_index) end # Returns the canonical build lookup index. The index maps Ruby member # name to [modeled_member_name, member_shape]. - # - # Example: - # Extension.member_index(shape) - # # => { ruby_name: ['wireName', member] } def member_index(shape) - (shape[KEY] || build_and_cache(shape))[:member_index] + shape[:schema_member_index] || resolve_aggregate(shape, :member_index) end # Returns the modeled media type, when present. - # - # Example: - # Extension.media_type(shape) - # # => 'application/octet-stream' def media_type(shape) - (shape[KEY] || build_and_cache(shape))[:media_type] + shape.fetch_metadata(:schema_media_type) do + shape.traits['smithy.api#mediaType'] + end end # Returns whether the sensitive trait is present. def sensitive?(shape) - (shape[KEY] || build_and_cache(shape))[:sensitive] + shape.fetch_metadata(:schema_sensitive) do + shape.traits.key?('smithy.api#sensitive') + end end # Returns whether the streaming trait is present. def streaming?(shape) - (shape[KEY] || build_and_cache(shape))[:streaming] + shape.fetch_metadata(:schema_streaming) do + shape.traits.key?('smithy.api#streaming') + end end # Returns whether the requires-length trait is present. def requires_length?(shape) - (shape[KEY] || build_and_cache(shape))[:requires_length] + shape.fetch_metadata(:schema_requires_length) do + shape.traits.key?('smithy.api#requiresLength') + end end def endpoint_host_prefix(operation) - (operation[KEY] || build_and_cache(operation))[:endpoint_host_prefix] + operation.fetch_metadata(:schema_endpoint_host_prefix) do + resolve_operation(operation, :endpoint_host_prefix) + end end def endpoint_host_prefix_plan(operation) - (operation[KEY] || build_and_cache(operation))[:endpoint_host_prefix_plan] + operation.fetch_metadata(:schema_endpoint_host_prefix_plan) do + resolve_operation(operation, :endpoint_host_prefix_plan) + end end def request_compression_encodings(operation) - (operation[KEY] || build_and_cache(operation))[:request_compression_encodings] + operation.fetch_metadata(:schema_request_compression_encodings) do + resolve_operation(operation, :request_compression_encodings) + end end def checksum_required?(operation) - (operation[KEY] || build_and_cache(operation))[:checksum_required] + operation.fetch_metadata(:schema_checksum_required) do + operation.traits.key?('smithy.api#httpChecksumRequired') + end end def long_polling?(operation) - (operation[KEY] || build_and_cache(operation))[:long_polling] + operation.fetch_metadata(:schema_long_polling) do + operation.traits.key?('smithy.api#longPoll') + end end def unsigned_payload?(operation) - (operation[KEY] || build_and_cache(operation))[:unsigned_payload] + operation.fetch_metadata(:schema_unsigned_payload) do + operation.traits.key?('aws.auth#unsignedPayload') + end end # Returns operation errors indexed by target shape name. - # - # Example: - # Extension.error_index(operation)['ResourceNotFound'] - # # => error_member def error_index(operation) - (operation[KEY] || build_and_cache(operation)).fetch(:error_index, {}.freeze) + operation[:schema_error_index] || resolve_operation(operation, :error_index) end def required_members(shape) - (shape[KEY] || build_and_cache(shape)).fetch(:required_members, [].freeze) + shape[:schema_required_members] || resolve_aggregate(shape, :required_members) end def host_label_index(shape) - (shape[KEY] || build_and_cache(shape)).fetch(:host_label_index, {}.freeze) + shape[:schema_host_label_index] || resolve_aggregate(shape, :host_label_index) end def idempotency_token_member(shape) - (shape[KEY] || build_and_cache(shape))[:idempotency_token_member] + shape.fetch_metadata(:schema_idempotency_token_member) do + resolve_aggregate(shape, :idempotency_token_member) + end end def streaming_member(shape) - (shape[KEY] || build_and_cache(shape))[:streaming_member] + shape.fetch_metadata(:schema_streaming_member) do + resolve_aggregate(shape, :streaming_member) + end end def streaming_member_unknown_length(shape) - (shape[KEY] || build_and_cache(shape))[:streaming_member_unknown_length] + shape.fetch_metadata(:schema_streaming_member_unknown_length) do + resolve_aggregate(shape, :streaming_member_unknown_length) + end end def event_stream_member(shape) - (shape[KEY] || build_and_cache(shape))[:event_stream_member] + shape.fetch_metadata(:schema_event_stream_member) do + resolve_aggregate(shape, :event_stream_member) + end end - # Returns the effective timestamp format, or +:default+ when the - # model does not select one. - # - # Example: - # Extension.timestamp_format(member) - # # => 'date-time' + # Returns the effective timestamp format, or +:default+ when the model + # does not select one. def timestamp_format(shape) - (shape[KEY] || build_and_cache(shape)).fetch(:timestamp_format, :default) + shape[:schema_timestamp_format] ||= resolve_timestamp_format(shape) 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 @@ -145,44 +139,32 @@ def each_member(shape, &block) end # Returns whether a collection may retain nil values. - # - # Example: - # Extension.sparse?(list) - # # => true def sparse?(shape) - shape.fetch_metadata(:sparse) do + shape.fetch_metadata(:schema_sparse) do shape.traits.key?('smithy.api#sparse') end end private - def build_and_cache(shape) - 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 - - def build_operation_metadata(operation) + def resolve_operation(operation, result) traits = operation.traits endpoint_host_prefix = traits.dig('smithy.api#endpoint', 'hostPrefix') - { - endpoint_host_prefix: endpoint_host_prefix, - endpoint_host_prefix_plan: build_endpoint_host_prefix_plan(operation, endpoint_host_prefix), - 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, - error_index: build_error_index(operation) - }.compact.freeze + endpoint_host_prefix_plan = build_endpoint_host_prefix_plan(operation, endpoint_host_prefix) + request_compression_encodings = traits.dig('smithy.api#requestCompression', 'encodings') + error_index = build_error_index(operation) + + operation[:schema_endpoint_host_prefix] = endpoint_host_prefix + operation[:schema_endpoint_host_prefix_plan] = endpoint_host_prefix_plan + operation[:schema_request_compression_encodings] = request_compression_encodings + operation[:schema_error_index] = error_index + + case result + when :endpoint_host_prefix then endpoint_host_prefix + when :endpoint_host_prefix_plan then endpoint_host_prefix_plan + when :request_compression_encodings then request_compression_encodings + when :error_index then error_index + end end def build_endpoint_host_prefix_plan(operation, host_prefix) @@ -215,29 +197,16 @@ def build_error_index(operation) end.freeze end - def build_shape_metadata(shape) - metadata = {} - add_media_type_metadata(metadata, shape) - add_boolean_trait_metadata(metadata, shape) - add_timestamp_metadata(metadata, shape) - metadata.freeze - end - - def build_member_metadata(member) - metadata = {} - 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 + def resolve_aggregate(shape, result) wire_index = {} member_index = {} required_members = [] host_label_index = {} + idempotency_token_member = nil + streaming_member = nil + event_stream_member = nil + streaming_member_unknown_length = nil shape.members.each do |ruby_name, member| modeled_name = member.name @@ -250,50 +219,49 @@ def build_aggregate_metadata(shape) 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') + idempotency_token_member ||= ruby_name if member.traits.key?('smithy.api#idempotencyToken') + target = member.target - next unless streaming_trait?(target) + next unless target.traits.key?('smithy.api#streaming') - metadata[:streaming_member] ||= member - metadata[:event_stream_member] ||= member if target.instance_of?(Shapes::UnionShape) - metadata[:streaming_member_unknown_length] ||= member unless requires_length_trait?(target) + streaming_member ||= member + event_stream_member ||= member if target.instance_of?(Shapes::UnionShape) + streaming_member_unknown_length ||= member unless target.traits.key?('smithy.api#requiresLength') end - 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 + wire_index.freeze + member_index.freeze + required_members.freeze + host_label_index.freeze + shape[:schema_wire_index] = wire_index + shape[:schema_member_index] = member_index + shape[:schema_required_members] = required_members + shape[:schema_host_label_index] = host_label_index + shape[:schema_idempotency_token_member] = idempotency_token_member + shape[:schema_streaming_member] = streaming_member + shape[:schema_event_stream_member] = event_stream_member + shape[:schema_streaming_member_unknown_length] = streaming_member_unknown_length + + case result + when :wire_index then wire_index + when :member_index then member_index + when :required_members then required_members + when :host_label_index then host_label_index + when :idempotency_token_member then idempotency_token_member + when :streaming_member then streaming_member + when :event_stream_member then event_stream_member + when :streaming_member_unknown_length then streaming_member_unknown_length + end end - def add_timestamp_metadata(metadata, shape) + def resolve_timestamp_format(shape) target = shape.target - return unless target.is_a?(Shapes::TimestampShape) + return :default unless target.is_a?(Shapes::TimestampShape) - metadata[:timestamp_format] = - shape.traits['smithy.api#timestampFormat'] || + shape.traits['smithy.api#timestampFormat'] || 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 end end end diff --git a/gems/smithy-schema/sig/smithy-schema/extension.rbs b/gems/smithy-schema/sig/smithy-schema/extension.rbs index c86f1a8d9..acc7340ac 100644 --- a/gems/smithy-schema/sig/smithy-schema/extension.rbs +++ b/gems/smithy-schema/sig/smithy-schema/extension.rbs @@ -1,19 +1,19 @@ module Smithy module Schema module Extension - 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]] def self.member_index: ((Shapes::StructureShape | Shapes::UnionShape) shape) -> Hash[Symbol, [String, Shapes::MemberShape]] 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.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.endpoint_host_prefix_plan: (Shapes::OperationShape operation) -> Array[String | Symbol]? 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.checksum_required?: (Shapes::OperationShape operation) -> bool + def self.long_polling?: (Shapes::OperationShape operation) -> bool + def self.unsigned_payload?: (Shapes::OperationShape operation) -> bool + def self.error_index: (Shapes::OperationShape operation) -> Hash[String, Shapes::MemberShape] 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? diff --git a/gems/smithy-schema/spec/smithy-schema/extension_spec.rb b/gems/smithy-schema/spec/smithy-schema/extension_spec.rb index 38db5e56f..171c262fa 100644 --- a/gems/smithy-schema/spec/smithy-schema/extension_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/extension_spec.rb @@ -15,6 +15,7 @@ module Schema expected_values = [:some_member, member] expect(described_class.wire_index(shape)).to eq('wireName' => expected_values) expect(described_class.wire_index(shape)).to be_frozen + expect(shape[:schema_wire_index]).to be(described_class.wire_index(shape)) end it 'ignores members that do not have a modeled member name' do @@ -43,7 +44,10 @@ module Schema describe '.sparse?' do it 'returns whether the sparse trait is present' do - expect(described_class.sparse?(Shapes::ListShape.new)).to be(false) + shape = Shapes::ListShape.new + + expect(described_class.sparse?(shape)).to be(false) + expect(shape[:schema_sparse]).to be(false) expect(described_class.sparse?(Shapes::ListShape.new(traits: { 'smithy.api#sparse' => {} }))).to be(true) end end @@ -55,6 +59,7 @@ module Schema operation = Shapes::OperationShape.new(errors: [error_member]) expect(described_class.error_index(operation)).to eq('ExampleError' => error_member) + expect(operation[:schema_error_index]).to be(described_class.error_index(operation)) end end @@ -78,6 +83,18 @@ module Schema ) expect(described_class.media_type(shape)).to eq('application/custom') + expect(shape[:schema_media_type]).to eq('application/custom') + end + + it 'caches absent boolean traits as false' do + shape = Shapes::BlobShape.new + + expect(described_class.sensitive?(shape)).to be(false) + expect(described_class.streaming?(shape)).to be(false) + expect(described_class.requires_length?(shape)).to be(false) + expect(shape[:schema_sensitive]).to be(false) + expect(shape[:schema_streaming]).to be(false) + expect(shape[:schema_requires_length]).to be(false) end end end From 7c03e5e6e31cc8795d5679fbf2ff8b8323e912b8 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Wed, 23 Sep 2026 07:53:34 -0700 Subject: [PATCH 28/36] refactor: simplify schema aggregate resolution --- .../lib/smithy-schema/extension.rb | 129 +++++++++--------- 1 file changed, 67 insertions(+), 62 deletions(-) diff --git a/gems/smithy-schema/lib/smithy-schema/extension.rb b/gems/smithy-schema/lib/smithy-schema/extension.rb index 0be431cd7..fc91b10f0 100644 --- a/gems/smithy-schema/lib/smithy-schema/extension.rb +++ b/gems/smithy-schema/lib/smithy-schema/extension.rb @@ -14,13 +14,13 @@ class << self # consumers. The index maps modeled member name to # [ruby_member_name, member_shape]. def wire_index(shape) - shape[:schema_wire_index] || resolve_aggregate(shape, :wire_index) + shape[:schema_wire_index] || resolve_aggregate(shape, :schema_wire_index) end # Returns the canonical build lookup index. The index maps Ruby member # name to [modeled_member_name, member_shape]. def member_index(shape) - shape[:schema_member_index] || resolve_aggregate(shape, :member_index) + shape[:schema_member_index] || resolve_aggregate(shape, :schema_member_index) end # Returns the modeled media type, when present. @@ -93,34 +93,34 @@ def error_index(operation) end def required_members(shape) - shape[:schema_required_members] || resolve_aggregate(shape, :required_members) + shape[:schema_required_members] || resolve_aggregate(shape, :schema_required_members) end def host_label_index(shape) - shape[:schema_host_label_index] || resolve_aggregate(shape, :host_label_index) + shape[:schema_host_label_index] || resolve_aggregate(shape, :schema_host_label_index) end def idempotency_token_member(shape) shape.fetch_metadata(:schema_idempotency_token_member) do - resolve_aggregate(shape, :idempotency_token_member) + resolve_aggregate(shape, :schema_idempotency_token_member) end end def streaming_member(shape) shape.fetch_metadata(:schema_streaming_member) do - resolve_aggregate(shape, :streaming_member) + resolve_aggregate(shape, :schema_streaming_member) end end def streaming_member_unknown_length(shape) shape.fetch_metadata(:schema_streaming_member_unknown_length) do - resolve_aggregate(shape, :streaming_member_unknown_length) + resolve_aggregate(shape, :schema_streaming_member_unknown_length) end end def event_stream_member(shape) shape.fetch_metadata(:schema_event_stream_member) do - resolve_aggregate(shape, :event_stream_member) + resolve_aggregate(shape, :schema_event_stream_member) end end @@ -132,10 +132,8 @@ def timestamp_format(shape) # Iterates modeled members with separate Ruby name and member-shape # arguments. With no block, returns the underlying enumerator. - def each_member(shape, &block) - return shape.members.each unless block - - shape.members.each { |name, member| block.call(name, member) } + def each_member(shape, &) + shape.members.each(&) end # Returns whether a collection may retain nil values. @@ -197,61 +195,68 @@ def build_error_index(operation) end.freeze end - # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity def resolve_aggregate(shape, result) - wire_index = {} - member_index = {} - required_members = [] - host_label_index = {} - idempotency_token_member = nil - streaming_member = nil - event_stream_member = nil - streaming_member_unknown_length = nil - + metadata = empty_aggregate_metadata shape.members.each do |ruby_name, member| - modeled_name = member.name - next unless modeled_name - - wire_index[modeled_name] = [ruby_name, member].freeze - member_index[ruby_name] = [modeled_name, member].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') - idempotency_token_member ||= ruby_name if member.traits.key?('smithy.api#idempotencyToken') - - target = member.target - next unless target.traits.key?('smithy.api#streaming') - - streaming_member ||= member - event_stream_member ||= member if target.instance_of?(Shapes::UnionShape) - streaming_member_unknown_length ||= member unless target.traits.key?('smithy.api#requiresLength') + next unless member.name + + index_aggregate_member(metadata, ruby_name, member) + index_streaming_member(metadata, member) end - wire_index.freeze - member_index.freeze - required_members.freeze - host_label_index.freeze - shape[:schema_wire_index] = wire_index - shape[:schema_member_index] = member_index - shape[:schema_required_members] = required_members - shape[:schema_host_label_index] = host_label_index - shape[:schema_idempotency_token_member] = idempotency_token_member - shape[:schema_streaming_member] = streaming_member - shape[:schema_event_stream_member] = event_stream_member - shape[:schema_streaming_member_unknown_length] = streaming_member_unknown_length + freeze_aggregate_metadata(metadata) + metadata.each { |key, value| shape[key] = value } + metadata.fetch(result) + end - case result - when :wire_index then wire_index - when :member_index then member_index - when :required_members then required_members - when :host_label_index then host_label_index - when :idempotency_token_member then idempotency_token_member - when :streaming_member then streaming_member - when :event_stream_member then event_stream_member - when :streaming_member_unknown_length then streaming_member_unknown_length - end + def empty_aggregate_metadata + { + schema_wire_index: {}, + schema_member_index: {}, + schema_required_members: [], + schema_host_label_index: {}, + schema_idempotency_token_member: nil, + schema_streaming_member: nil, + schema_event_stream_member: nil, + schema_streaming_member_unknown_length: nil + } + end + + def index_aggregate_member(metadata, ruby_name, member) + modeled_name = member.name + metadata[:schema_wire_index][modeled_name] = [ruby_name, member].freeze + metadata[:schema_member_index][ruby_name] = [modeled_name, member].freeze + metadata[:schema_required_members] << ruby_name if required_member?(member) + metadata[:schema_host_label_index][modeled_name] = ruby_name if member.traits.key?('smithy.api#hostLabel') + index_idempotency_token_member(metadata, ruby_name, member) + end + + def index_idempotency_token_member(metadata, ruby_name, member) + return unless member.traits.key?('smithy.api#idempotencyToken') + + metadata[:schema_idempotency_token_member] ||= ruby_name + end + + def required_member?(member) + member.traits.key?('smithy.api#required') && + !member.traits.key?('smithy.api#clientOptional') + end + + def index_streaming_member(metadata, member) + target = member.target + return unless target.traits.key?('smithy.api#streaming') + + metadata[:schema_streaming_member] ||= member + metadata[:schema_event_stream_member] ||= member if target.instance_of?(Shapes::UnionShape) + return if target.traits.key?('smithy.api#requiresLength') + + metadata[:schema_streaming_member_unknown_length] ||= member + end + + def freeze_aggregate_metadata(metadata) + metadata.values_at( + :schema_wire_index, :schema_member_index, :schema_required_members, :schema_host_label_index + ).each(&:freeze) end def resolve_timestamp_format(shape) From a59de30ae4985898dd27ca9071858c6dc3757865 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Wed, 23 Sep 2026 07:56:57 -0700 Subject: [PATCH 29/36] perf: flatten JSON extension metadata --- gems/smithy-json/lib/smithy-json/extension.rb | 96 ++++--------------- .../smithy-json/sig/smithy-json/extension.rbs | 3 - .../spec/smithy-json/extension_spec.rb | 18 +--- 3 files changed, 26 insertions(+), 91 deletions(-) diff --git a/gems/smithy-json/lib/smithy-json/extension.rb b/gems/smithy-json/lib/smithy-json/extension.rb index de0d5ab9b..3d52498d5 100644 --- a/gems/smithy-json/lib/smithy-json/extension.rb +++ b/gems/smithy-json/lib/smithy-json/extension.rb @@ -4,104 +4,50 @@ module Smithy module Json # JSON-specific lookup helpers and cached serde metadata. # - # Raw Smithy trait data remains on +member.traits+ with string keys. This - # extension resolves JSON wire names and member indexes on demand, then - # caches them under +object[KEY]+. Generic shape and trait metadata remains - # owned by Schema::Extension. + # Raw Smithy trait data remains on +member.traits+ with string keys. + # Resolved JSON values are cached as flat, JSON-prefixed keys on their + # owning shape or member. # @api private module Extension - KEY = :json - EMPTY_METADATA = {}.freeze - class << self - # Resolves and returns JSON metadata for a structure, union, or member. - # - # Example: - # Extension.fetch(member) - # # => { json_name: 'wireName' } - def fetch(shape) - shape[KEY] || build_and_cache(shape) - end - - # Returns the JSON parse lookup index cached in structure or union - # metadata. - # - # The index maps: - # - resolved JSON wire name - # - to [ruby_member_name, member_shape] - # - # Example: - # Extension.wire_index(shape) - # # => { 'wireName' => [:ruby_name, member] } + # Returns the JSON parse lookup index cached on a structure or union. def wire_index(shape) - (shape[KEY] || build_and_cache(shape))[:json_wire_index] + shape[:json_wire_index] || resolve_indexes(shape, :json_wire_index) end - # Returns the JSON build lookup index cached in structure or union - # metadata. - # - # The index maps: - # - Ruby member name - # - to [resolved JSON wire name, member_shape] - # - # Example: - # Extension.member_index(shape) - # # => { ruby_name: ['wireName', member] } + # Returns the JSON build lookup index cached on a structure or union. def member_index(shape) - (shape[KEY] || build_and_cache(shape))[:json_member_index] + shape[:json_member_index] || resolve_indexes(shape, :json_member_index) end - # Returns the effective JSON member name: +smithy.api#jsonName+ when - # present, otherwise the modeled member name. - # - # Example: - # Extension.wire_name(member) - # # => 'wireName' + # Returns the effective JSON member name. def wire_name(member) - (member[KEY] || build_and_cache(member))[:json_name] + member.fetch_metadata(:json_name) do + member.traits['smithy.api#jsonName'] || member.name + end end # Returns the resolved timestamp format for JSON serialization. - # - # Example: - # Extension.timestamp_format(member) - # # => 'date-time' def timestamp_format(shape) Schema::Extension.timestamp_format(shape) end private - def build_and_cache(shape) - shape[KEY] = - case shape - when Schema::Shapes::StructureShape, Schema::Shapes::UnionShape - build_structure_metadata(shape) - when Schema::Shapes::MemberShape - build_member_metadata(shape) - else - EMPTY_METADATA - end - end - - def build_structure_metadata(shape) - json_wire_index = {} - json_member_index = {} - + def resolve_indexes(shape, result) + wire_index = {} + member_index = {} Schema::Extension.each_member(shape) do |member_name, member_shape| json_name = wire_name(member_shape) - json_wire_index[json_name] = [member_name, member_shape].freeze - json_member_index[member_name] = [json_name, member_shape].freeze + wire_index[json_name] = [member_name, member_shape].freeze + member_index[member_name] = [json_name, member_shape].freeze end - { - json_wire_index: json_wire_index.freeze, - json_member_index: json_member_index.freeze - }.freeze - end - - def build_member_metadata(member) - { json_name: member.traits['smithy.api#jsonName'] || member.name }.freeze + wire_index.freeze + member_index.freeze + shape[:json_wire_index] = wire_index + shape[:json_member_index] = member_index + result == :json_wire_index ? wire_index : member_index end end end diff --git a/gems/smithy-json/sig/smithy-json/extension.rbs b/gems/smithy-json/sig/smithy-json/extension.rbs index 6c947789b..d0374543f 100644 --- a/gems/smithy-json/sig/smithy-json/extension.rbs +++ b/gems/smithy-json/sig/smithy-json/extension.rbs @@ -2,9 +2,6 @@ module Smithy module Json module Extension type aggregate_shape = Schema::Shapes::StructureShape | Schema::Shapes::UnionShape - type serde_shape = aggregate_shape | Schema::Shapes::MemberShape - - def self.fetch: ((Schema::Shapes::Shape | Schema::Shapes::MemberShape) shape) -> Hash[Symbol, untyped] def self.wire_index: (aggregate_shape shape) -> Hash[String?, [Symbol, Schema::Shapes::MemberShape]] def self.member_index: (aggregate_shape shape) -> Hash[Symbol, [String?, Schema::Shapes::MemberShape]] def self.timestamp_format: ((Schema::Shapes::Shape | Schema::Shapes::MemberShape) shape) -> (String | Symbol) diff --git a/gems/smithy-json/spec/smithy-json/extension_spec.rb b/gems/smithy-json/spec/smithy-json/extension_spec.rb index d2dda4825..f73848f6a 100644 --- a/gems/smithy-json/spec/smithy-json/extension_spec.rb +++ b/gems/smithy-json/spec/smithy-json/extension_spec.rb @@ -30,8 +30,9 @@ module Json 'wireName' => [:json_named, json_named_member] ) expect(described_class.wire_index(shape)).to be_frozen - expect(plain_member[:json][:json_name]).to eq('plainName') - expect(json_named_member[:json][:json_name]).to eq('wireName') + expect(shape[:json_wire_index]).to be(described_class.wire_index(shape)) + expect(plain_member[:json_name]).to eq('plainName') + expect(json_named_member[:json_name]).to eq('wireName') end end @@ -52,21 +53,12 @@ module Json describe '.wire_name' do it 'returns jsonName when present' do expect(described_class.wire_name(json_named_member)).to eq('wireName') - expect(json_named_member[:json][:json_name]).to eq('wireName') + expect(json_named_member[:json_name]).to eq('wireName') end it 'falls back to the member name' do expect(described_class.wire_name(plain_member)).to eq('plainName') - expect(plain_member[:json][:json_name]).to eq('plainName') - end - end - - describe '.fetch' do - it 'caches a truthy empty payload for unsupported shape kinds' do - shape = Schema::Shapes::StringShape.new - - expect(described_class.fetch(shape)).to be_empty - expect(described_class.fetch(shape)).to be(shape[:json]) + expect(plain_member[:json_name]).to eq('plainName') end end end From 9e75a6b9d6640717aab8256cf5da0a3c30044760 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Wed, 23 Sep 2026 08:04:40 -0700 Subject: [PATCH 30/36] perf: flatten HTTP extension metadata --- .../lib/smithy-client/http_extension.rb | 76 +++++++++++-------- .../sig/smithy-client/http_extension.rbs | 5 +- .../spec/smithy-client/http_extension_spec.rb | 24 +++--- 3 files changed, 60 insertions(+), 45 deletions(-) diff --git a/gems/smithy-client/lib/smithy-client/http_extension.rb b/gems/smithy-client/lib/smithy-client/http_extension.rb index 816372465..3e6a7fdb0 100644 --- a/gems/smithy-client/lib/smithy-client/http_extension.rb +++ b/gems/smithy-client/lib/smithy-client/http_extension.rb @@ -4,9 +4,9 @@ module Smithy module Client # Cached Smithy HTTP binding metadata. # - # Metadata is stored under +shape[:http]+. Operation metadata contains - # +:method+, +:path+, +:static_query+, and +:response_code+. Structure - # metadata contains ordered binding entries: + # Resolved values are cached as flat, HTTP-prefixed keys on their owning + # operation or structure. Structure metadata contains ordered binding + # entries: # - headers and queries: +[ruby_name, member_shape, wire_name]+ # - prefix headers: +[ruby_name, member_shape, prefix]+ # - query params: +[ruby_name, member_shape]+ @@ -18,9 +18,6 @@ module Client # +Schema::Extension.media_type+. # @api private module HttpExtension - KEY = :http - EMPTY_ARRAY = [].freeze - EMPTY_HASH = {}.freeze BINDING_WRITERS = { 'smithy.api#httpHeader' => :add_header, 'smithy.api#httpPrefixHeaders' => :add_prefix_header, @@ -32,8 +29,22 @@ module HttpExtension }.freeze class << self - def fetch(shape) - shape[KEY] || build_and_cache(shape) + def method(operation) + operation[:http_method] || resolve_operation(operation, :http_method) + end + + def path(operation) + operation[:http_path] || resolve_operation(operation, :http_path) + end + + def static_query(operation) + operation.fetch_metadata(:http_static_query) do + resolve_operation(operation, :http_static_query) + end + end + + def response_code(operation) + operation[:http_response_code] || resolve_operation(operation, :http_response_code) end # Returns header bindings as: @@ -43,7 +54,7 @@ def fetch(shape) # HttpExtension.header_members(shape) # # => [[:request_id, member, 'X-Request-Id']] def header_members(shape) - (shape[KEY] || build_and_cache(shape)).fetch(:header_members, EMPTY_ARRAY) + shape[:http_header_members] || resolve_bindings(shape, :http_header_members) end # Returns the prefix-header binding as: @@ -53,7 +64,9 @@ def header_members(shape) # HttpExtension.prefix_header_member(shape) # # => [:metadata, member, 'x-amz-meta-'] def prefix_header_member(shape) - (shape[KEY] || build_and_cache(shape))[:prefix_header_member] + shape.fetch_metadata(:http_prefix_header_member) do + resolve_bindings(shape, :http_prefix_header_member) + end end # Returns query bindings as: @@ -63,7 +76,7 @@ def prefix_header_member(shape) # HttpExtension.query_members(shape) # # => [[:page_size, member, 'pageSize']] def query_members(shape) - (shape[KEY] || build_and_cache(shape)).fetch(:query_members, EMPTY_ARRAY) + shape[:http_query_members] || resolve_bindings(shape, :http_query_members) end # Returns the query-params binding as: @@ -73,7 +86,9 @@ def query_members(shape) # HttpExtension.query_params_member(shape) # # => [:filters, member] def query_params_member(shape) - (shape[KEY] || build_and_cache(shape))[:query_params_member] + shape.fetch_metadata(:http_query_params_member) do + resolve_bindings(shape, :http_query_params_member) + end end # Returns labels indexed by modeled member name. @@ -82,7 +97,7 @@ def query_params_member(shape) # HttpExtension.label_index(shape) # # => { 'bucket' => [:bucket, member] } def label_index(shape) - (shape[KEY] || build_and_cache(shape)).fetch(:label_index, EMPTY_HASH) + shape[:http_label_index] || resolve_bindings(shape, :http_label_index) end # Returns members serialized in the document body. @@ -91,7 +106,7 @@ def label_index(shape) # HttpExtension.body_members(shape) # # => [[:name, member]] def body_members(shape) - (shape[KEY] || build_and_cache(shape)).fetch(:body_members, EMPTY_ARRAY) + shape[:http_body_members] || resolve_bindings(shape, :http_body_members) end # Returns the payload binding as: @@ -101,7 +116,9 @@ def body_members(shape) # HttpExtension.payload_member(shape) # # => [:body, member, :raw, 'application/octet-stream'] def payload_member(shape) - (shape[KEY] || build_and_cache(shape))[:payload_member] + shape.fetch_metadata(:http_payload_member) do + resolve_bindings(shape, :http_payload_member) + end end # Returns the response-code binding as: @@ -111,36 +128,31 @@ def payload_member(shape) # HttpExtension.response_code_member(shape) # # => [:status_code, member] def response_code_member(shape) - (shape[KEY] || build_and_cache(shape))[:response_code_member] + shape.fetch_metadata(:http_response_code_member) do + resolve_bindings(shape, :http_response_code_member) + end end private - def build_and_cache(shape) - shape[KEY] = - if shape.is_a?(Schema::Shapes::OperationShape) - operation_metadata(shape) - elsif shape.respond_to?(:members) - shape_metadata(shape) - else - EMPTY_HASH - end - end - - def operation_metadata(operation) + def resolve_operation(operation, result) http = operation.traits['smithy.api#http'] || {} path, static_query = (http['uri'] || '/').split('?', 2) - { method: http['method'] || 'POST', path: path, static_query: static_query, - response_code: http.fetch('code', 200) }.compact.freeze + operation[:http_method] = http['method'] || 'POST' + operation[:http_path] = path + operation[:http_static_query] = static_query + operation[:http_response_code] = http.fetch('code', 200) + operation[result] end - def shape_metadata(shape) + def resolve_bindings(shape, result) metadata = { header_members: [], query_members: [], label_index: {}, body_members: [] } shape.members.each do |name, member| add_member_binding(metadata, name, member) end metadata.each_value { |value| value.freeze if value.respond_to?(:freeze) } - metadata.freeze + metadata.each { |key, value| shape[:"http_#{key}"] = value } + shape[result] end def add_member_binding(metadata, name, member) diff --git a/gems/smithy-client/sig/smithy-client/http_extension.rbs b/gems/smithy-client/sig/smithy-client/http_extension.rbs index 4acd8e63d..e67066e02 100644 --- a/gems/smithy-client/sig/smithy-client/http_extension.rbs +++ b/gems/smithy-client/sig/smithy-client/http_extension.rbs @@ -1,7 +1,10 @@ module Smithy module Client module HttpExtension - def self.fetch: (Schema::Shapes::Shape shape) -> Hash[Symbol, untyped] + def self.method: (Schema::Shapes::OperationShape operation) -> String + def self.path: (Schema::Shapes::OperationShape operation) -> String + def self.static_query: (Schema::Shapes::OperationShape operation) -> String? + def self.response_code: (Schema::Shapes::OperationShape operation) -> Integer def self.header_members: (Schema::Shapes::Shape shape) -> Array[untyped] def self.prefix_header_member: (Schema::Shapes::Shape shape) -> untyped def self.query_members: (Schema::Shapes::Shape shape) -> Array[untyped] diff --git a/gems/smithy-client/spec/smithy-client/http_extension_spec.rb b/gems/smithy-client/spec/smithy-client/http_extension_spec.rb index 38ab4662d..453736d3f 100644 --- a/gems/smithy-client/spec/smithy-client/http_extension_spec.rb +++ b/gems/smithy-client/spec/smithy-client/http_extension_spec.rb @@ -5,22 +5,19 @@ module Smithy module Client describe HttpExtension do - it 'caches empty HTTP metadata for unsupported shapes' do - shape = Schema::Shapes::StringShape.new - - expect(described_class.fetch(shape)).to eq({}) - expect(described_class.fetch(shape)).to be(shape[:http]) - end - - it 'caches HTTP operation metadata' do + it 'caches flat HTTP operation metadata' do operation = Schema::Shapes::OperationShape.new( traits: { 'smithy.api#http' => { 'method' => 'GET', 'uri' => '/things?x=1', 'code' => 204 } } ) - expect(described_class.fetch(operation)).to include( - method: 'GET', path: '/things', static_query: 'x=1', response_code: 204 - ) - expect(described_class.fetch(operation)).to be(described_class.fetch(operation)) + expect(described_class.method(operation)).to eq('GET') + expect(described_class.path(operation)).to eq('/things') + expect(described_class.static_query(operation)).to eq('x=1') + expect(described_class.response_code(operation)).to eq(204) + expect(operation[:http_method]).to eq('GET') + expect(operation[:http_path]).to eq('/things') + expect(operation[:http_static_query]).to eq('x=1') + expect(operation[:http_response_code]).to eq(204) end it 'indexes member bindings and payload media types' do @@ -66,6 +63,9 @@ module Client expect(described_class.query_params_member(shape)).to eq([:query_params, query_params]) expect(described_class.payload_member(shape).last).to eq('application/custom') expect(described_class.response_code_member(shape)).to eq([:response_code, response_code]) + expect(shape[:http_header_members]).to be(described_class.header_members(shape)) + expect(shape[:http_query_members]).to be(described_class.query_members(shape)) + expect(shape[:http_body_members]).to be(described_class.body_members(shape)) end end end From 2a2b8d3ff09731d30c7178957602e9a04c3f5aef Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Wed, 23 Sep 2026 10:02:31 -0700 Subject: [PATCH 31/36] perf: resolve operation metadata independently --- .../lib/smithy-schema/extension.rb | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/gems/smithy-schema/lib/smithy-schema/extension.rb b/gems/smithy-schema/lib/smithy-schema/extension.rb index fc91b10f0..2bb0cd91a 100644 --- a/gems/smithy-schema/lib/smithy-schema/extension.rb +++ b/gems/smithy-schema/lib/smithy-schema/extension.rb @@ -53,19 +53,19 @@ def requires_length?(shape) def endpoint_host_prefix(operation) operation.fetch_metadata(:schema_endpoint_host_prefix) do - resolve_operation(operation, :endpoint_host_prefix) + resolve_endpoint(operation, :schema_endpoint_host_prefix) end end def endpoint_host_prefix_plan(operation) operation.fetch_metadata(:schema_endpoint_host_prefix_plan) do - resolve_operation(operation, :endpoint_host_prefix_plan) + resolve_endpoint(operation, :schema_endpoint_host_prefix_plan) end end def request_compression_encodings(operation) operation.fetch_metadata(:schema_request_compression_encodings) do - resolve_operation(operation, :request_compression_encodings) + operation.traits.dig('smithy.api#requestCompression', 'encodings') end end @@ -89,7 +89,7 @@ def unsigned_payload?(operation) # Returns operation errors indexed by target shape name. def error_index(operation) - operation[:schema_error_index] || resolve_operation(operation, :error_index) + operation[:schema_error_index] ||= build_error_index(operation) end def required_members(shape) @@ -145,24 +145,13 @@ def sparse?(shape) private - def resolve_operation(operation, result) - traits = operation.traits - endpoint_host_prefix = traits.dig('smithy.api#endpoint', 'hostPrefix') + def resolve_endpoint(operation, result) + endpoint_host_prefix = operation.traits.dig('smithy.api#endpoint', 'hostPrefix') endpoint_host_prefix_plan = build_endpoint_host_prefix_plan(operation, endpoint_host_prefix) - request_compression_encodings = traits.dig('smithy.api#requestCompression', 'encodings') - error_index = build_error_index(operation) operation[:schema_endpoint_host_prefix] = endpoint_host_prefix operation[:schema_endpoint_host_prefix_plan] = endpoint_host_prefix_plan - operation[:schema_request_compression_encodings] = request_compression_encodings - operation[:schema_error_index] = error_index - - case result - when :endpoint_host_prefix then endpoint_host_prefix - when :endpoint_host_prefix_plan then endpoint_host_prefix_plan - when :request_compression_encodings then request_compression_encodings - when :error_index then error_index - end + operation[result] end def build_endpoint_host_prefix_plan(operation, host_prefix) From a6ec87f42c07f6dc3b80daadcc0fd7dd34633f6f Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Wed, 23 Sep 2026 11:12:05 -0700 Subject: [PATCH 32/36] perf: reuse serde workers across calls --- gems/smithy-cbor/lib/smithy-cbor/codec.rb | 7 +- .../spec/smithy-cbor/codec_spec.rb | 7 ++ gems/smithy-json/lib/smithy-json/codec.rb | 7 +- .../spec/smithy-json/codec_spec.rb | 7 ++ gems/smithy-xml/lib/smithy-xml/builder.rb | 73 ++++++++++--------- gems/smithy-xml/lib/smithy-xml/codec.rb | 7 +- gems/smithy-xml/spec/smithy-xml/codec_spec.rb | 21 ++++++ 7 files changed, 85 insertions(+), 44 deletions(-) diff --git a/gems/smithy-cbor/lib/smithy-cbor/codec.rb b/gems/smithy-cbor/lib/smithy-cbor/codec.rb index 2c95508e0..e17957bda 100644 --- a/gems/smithy-cbor/lib/smithy-cbor/codec.rb +++ b/gems/smithy-cbor/lib/smithy-cbor/codec.rb @@ -6,14 +6,15 @@ module Cbor class Codec # @param [Hash] options def initialize(options = {}) - @options = options + @builder = Builder.new(options).freeze + @parser = Parser.new(options).freeze end # @param [Shape] shape # @param [Object] data # @return [String, nil] def build(shape, data) - Builder.new(@options).build(shape, data) + @builder.build(shape, data) end # @param [Shape] shape @@ -21,7 +22,7 @@ def build(shape, data) # @param [Object, nil] result (nil) # @return [Object, nil] def parse(shape, bytes, result = nil) - Parser.new(@options).parse(shape, bytes, result) + @parser.parse(shape, bytes, result) end end end diff --git a/gems/smithy-cbor/spec/smithy-cbor/codec_spec.rb b/gems/smithy-cbor/spec/smithy-cbor/codec_spec.rb index 350973f51..9d1d044e1 100644 --- a/gems/smithy-cbor/spec/smithy-cbor/codec_spec.rb +++ b/gems/smithy-cbor/spec/smithy-cbor/codec_spec.rb @@ -9,6 +9,13 @@ module Cbor let(:sample_schema) { SchemaHelper.sample_schema(shapes: shapes) } let(:structure_shape) { sample_schema.const_get(:Structure) } + it 'freezes its reusable workers' do + codec = described_class.new + + expect(codec.instance_variable_get(:@builder)).to be_frozen + expect(codec.instance_variable_get(:@parser)).to be_frozen + end + it 'reuses the same codec instance across build calls without leaking builder state' do codec = described_class.new diff --git a/gems/smithy-json/lib/smithy-json/codec.rb b/gems/smithy-json/lib/smithy-json/codec.rb index 0ad2c6d3c..ddcda15dd 100644 --- a/gems/smithy-json/lib/smithy-json/codec.rb +++ b/gems/smithy-json/lib/smithy-json/codec.rb @@ -6,14 +6,15 @@ module Json class Codec # @param [Hash] options def initialize(options = {}) - @options = options + @builder = Builder.new(options).freeze + @parser = Parser.new(options).freeze end # @param [Shape] shape # @param [Object] data # @return [String, nil] def build(shape, data) - Builder.new(@options).build(shape, data) + @builder.build(shape, data) end # @param [Shape] shape @@ -21,7 +22,7 @@ def build(shape, data) # @param [Object, nil] result (nil) # @return [Object, nil] def parse(shape, bytes, result = nil) - Parser.new(@options).parse(shape, bytes, result) + @parser.parse(shape, bytes, result) end end end diff --git a/gems/smithy-json/spec/smithy-json/codec_spec.rb b/gems/smithy-json/spec/smithy-json/codec_spec.rb index b2e7dd1f6..033f278fc 100644 --- a/gems/smithy-json/spec/smithy-json/codec_spec.rb +++ b/gems/smithy-json/spec/smithy-json/codec_spec.rb @@ -9,6 +9,13 @@ module Json let(:sample_schema) { SchemaHelper.sample_schema(shapes: shapes) } let(:structure_shape) { sample_schema.const_get(:Structure) } + it 'freezes its reusable workers' do + codec = described_class.new + + expect(codec.instance_variable_get(:@builder)).to be_frozen + expect(codec.instance_variable_get(:@parser)).to be_frozen + end + it 'reuses the same codec instance across build calls without leaking builder state' do codec = described_class.new diff --git a/gems/smithy-xml/lib/smithy-xml/builder.rb b/gems/smithy-xml/lib/smithy-xml/builder.rb index 7bd80618d..95911b667 100644 --- a/gems/smithy-xml/lib/smithy-xml/builder.rb +++ b/gems/smithy-xml/lib/smithy-xml/builder.rb @@ -6,32 +6,35 @@ module Smithy module Xml # @api private class Builder + MAP_ENTRY_SHAPE = Schema::Shapes::MemberShape.new( + target: Schema::Shapes::MapShape.new + ).freeze + def initialize(options = {}) @indent = options.fetch(:indent, '') @pad = options.fetch(:pad, '') @default_timestamp = options.fetch(:default_timestamp, 'date-time') - @map_entry_shape = Schema::Shapes::MemberShape.new(target: Schema::Shapes::MapShape.new) end def build(shape, data, output = nil) output ||= [] - @builder = DocBuilder.new(output: output, indent: @indent, pad: @pad) - structure(Extension.structure_name(shape), shape, data) + builder = DocBuilder.new(output: output, indent: @indent, pad: @pad) + structure(builder, Extension.structure_name(shape), shape, data) output.join end private - def build_shape(name, shape, value) + def build_shape(builder, name, shape, value) target_shape = shape.target case target_shape - when Schema::Shapes::BlobShape then node(name, shape, blob(value)) - when Schema::Shapes::ListShape then list(name, shape, value) - when Schema::Shapes::MapShape then map(name, shape, value) - when Schema::Shapes::StructureShape then structure(name, shape, value) - when Schema::Shapes::TimestampShape then node(name, shape, timestamp(shape, value)) - when Schema::Shapes::UnionShape then union(name, shape, value) - else node(name, shape, value.to_s) + when Schema::Shapes::BlobShape then node(builder, name, shape, blob(value)) + when Schema::Shapes::ListShape then list(builder, name, shape, value) + when Schema::Shapes::MapShape then map(builder, name, shape, value) + when Schema::Shapes::StructureShape then structure(builder, name, shape, value) + when Schema::Shapes::TimestampShape then node(builder, name, shape, timestamp(shape, value)) + when Schema::Shapes::UnionShape then union(builder, name, shape, value) + else node(builder, name, shape, value.to_s) end end @@ -39,60 +42,60 @@ def blob(value) Base64.strict_encode64(value.respond_to?(:read) ? value.read : value) end - def list(name, shape, values) + def list(builder, name, shape, values) member_shape = shape.target.member flattened = Extension.flattened?(shape) if flattened values.each do |value| - build_shape(name, member_shape, value) + build_shape(builder, name, member_shape, value) end else member_name = Extension.wire_name(member_shape) - node(name, shape) do + node(builder, name, shape) do values.each do |value| - build_shape(member_name, member_shape, value) + build_shape(builder, member_name, member_shape, value) end end end end - def map(name, shape, values) + def map(builder, name, shape, values) flattened = Extension.flattened?(shape) if flattened - flat_map_entries(name, shape, values) + flat_map_entries(builder, name, shape, values) else key_name, key_member, value_name, value_member = Extension.map_parts(shape) - node(name, shape) do + node(builder, name, shape) do values.each do |key, value| - node('entry', @map_entry_shape) do - build_shape(key_name, key_member, key) - build_shape(value_name, value_member, value) + node(builder, 'entry', MAP_ENTRY_SHAPE) do + build_shape(builder, key_name, key_member, key) + build_shape(builder, value_name, value_member, value) end end end end end - def flat_map_entries(name, shape, values) + def flat_map_entries(builder, name, shape, values) key_name, key_member, value_name, value_member = Extension.map_parts(shape) values.each do |key, value| - node(name, shape) do - build_shape(key_name, key_member, key) - build_shape(value_name, value_member, value) + node(builder, name, shape) do + build_shape(builder, key_name, key_member, key) + build_shape(builder, value_name, value_member, value) end end end - def structure(name, shape, values) - return node(name, shape) if values.empty? + def structure(builder, name, shape, values) + return node(builder, name, shape) if values.empty? - node(name, shape, structure_attrs(shape, values)) do + node(builder, name, shape, structure_attrs(shape, values)) do element_members = Extension.element_members(shape.target) element_members.each do |member_name, xml_name, member_shape| member_value = values[member_name] next if member_value.nil? - build_shape(xml_name, member_shape, member_value) + build_shape(builder, xml_name, member_shape, member_value) end end end @@ -120,8 +123,8 @@ def timestamp(shape, value) end end - def union(name, shape, values) - return node(name, shape) if values.empty? + def union(builder, name, shape, values) + return node(builder, name, shape) if values.empty? if values.is_a?(Schema::Union) key = values.member @@ -129,9 +132,9 @@ def union(name, shape, values) else key, value = values.first end - node(name, shape, structure_attrs(shape, values)) do + node(builder, name, shape, structure_attrs(shape, values)) do member_shape = shape.target.member(key) - build_shape(Extension.wire_name(member_shape), member_shape, value) if member_shape + build_shape(builder, Extension.wire_name(member_shape), member_shape, value) if member_shape end end @@ -145,14 +148,14 @@ def union(name, shape, values) # Pass a block if you want to nest XML nodes inside. When doing this, # you may *not* pass a value to the `args` list. # - def node(name, shape, *args, &) + def node(builder, name, shape, *args, &) attrs = args.last.is_a?(Hash) ? args.pop : {} namespace_attrs = Extension.namespace_attrs(shape) if namespace_attrs attrs = attrs.empty? ? namespace_attrs : namespace_attrs.merge(attrs) end args << attrs - @builder.node(name, *args, &) + builder.node(name, *args, &) end end end diff --git a/gems/smithy-xml/lib/smithy-xml/codec.rb b/gems/smithy-xml/lib/smithy-xml/codec.rb index 23ae8d86e..190eea888 100644 --- a/gems/smithy-xml/lib/smithy-xml/codec.rb +++ b/gems/smithy-xml/lib/smithy-xml/codec.rb @@ -6,7 +6,8 @@ module Xml class Codec # @param [Hash] options def initialize(options = {}) - @options = options + @builder = Builder.new(options).freeze + @parser = Parser.new(options).freeze end # @param [Shape] shape @@ -14,7 +15,7 @@ def initialize(options = {}) # @param [Array, nil] output (nil) # @return [String, nil] def build(shape, data, output = nil) - Builder.new(@options).build(shape, data, output) + @builder.build(shape, data, output) end # @param [Shape] shape @@ -22,7 +23,7 @@ def build(shape, data, output = nil) # @param [Object, nil] result (nil) # @return [Object, nil] def parse(shape, bytes, result = nil) - Parser.new(@options).parse(shape, bytes, result) + @parser.parse(shape, bytes, result) end end end diff --git a/gems/smithy-xml/spec/smithy-xml/codec_spec.rb b/gems/smithy-xml/spec/smithy-xml/codec_spec.rb index 651417e6c..cfece3877 100644 --- a/gems/smithy-xml/spec/smithy-xml/codec_spec.rb +++ b/gems/smithy-xml/spec/smithy-xml/codec_spec.rb @@ -9,6 +9,13 @@ module Xml let(:sample_schema) { SchemaHelper.sample_schema(shapes: shapes) } let(:structure_shape) { sample_schema.const_get(:Structure) } + it 'freezes its reusable workers' do + codec = described_class.new + + expect(codec.instance_variable_get(:@builder)).to be_frozen + expect(codec.instance_variable_get(:@parser)).to be_frozen + end + it 'reuses the same codec instance across build calls without leaking builder state' do codec = described_class.new @@ -28,6 +35,20 @@ module Xml expect(first.to_h).to eq(string: 'first') expect(second.to_h).to eq(integer: 123) end + + it 'supports concurrent builds on the same codec instance' do + codec = described_class.new + builds = 20.times.map do |i| + Thread.new do + value = "value-#{i}" + codec.build(structure_shape, string: value) + end + end + + expect(builds.map(&:value)).to eq( + 20.times.map { |i| "value-#{i}" } + ) + end end end end From 60958bfb9056f4e00715f431796896ff30d6d9fa Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Wed, 23 Sep 2026 12:46:52 -0700 Subject: [PATCH 33/36] fix: avoid HTTP extension method collision --- gems/smithy-client/lib/smithy-client/http_extension.rb | 2 +- gems/smithy-client/sig/smithy-client/http_extension.rbs | 2 +- gems/smithy-client/spec/smithy-client/http_extension_spec.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gems/smithy-client/lib/smithy-client/http_extension.rb b/gems/smithy-client/lib/smithy-client/http_extension.rb index 3e6a7fdb0..4db067036 100644 --- a/gems/smithy-client/lib/smithy-client/http_extension.rb +++ b/gems/smithy-client/lib/smithy-client/http_extension.rb @@ -29,7 +29,7 @@ module HttpExtension }.freeze class << self - def method(operation) + def http_method(operation) operation[:http_method] || resolve_operation(operation, :http_method) end diff --git a/gems/smithy-client/sig/smithy-client/http_extension.rbs b/gems/smithy-client/sig/smithy-client/http_extension.rbs index e67066e02..52791f146 100644 --- a/gems/smithy-client/sig/smithy-client/http_extension.rbs +++ b/gems/smithy-client/sig/smithy-client/http_extension.rbs @@ -1,7 +1,7 @@ module Smithy module Client module HttpExtension - def self.method: (Schema::Shapes::OperationShape operation) -> String + def self.http_method: (Schema::Shapes::OperationShape operation) -> String def self.path: (Schema::Shapes::OperationShape operation) -> String def self.static_query: (Schema::Shapes::OperationShape operation) -> String? def self.response_code: (Schema::Shapes::OperationShape operation) -> Integer diff --git a/gems/smithy-client/spec/smithy-client/http_extension_spec.rb b/gems/smithy-client/spec/smithy-client/http_extension_spec.rb index 453736d3f..61162de30 100644 --- a/gems/smithy-client/spec/smithy-client/http_extension_spec.rb +++ b/gems/smithy-client/spec/smithy-client/http_extension_spec.rb @@ -10,7 +10,7 @@ module Client traits: { 'smithy.api#http' => { 'method' => 'GET', 'uri' => '/things?x=1', 'code' => 204 } } ) - expect(described_class.method(operation)).to eq('GET') + expect(described_class.http_method(operation)).to eq('GET') expect(described_class.path(operation)).to eq('/things') expect(described_class.static_query(operation)).to eq('x=1') expect(described_class.response_code(operation)).to eq(204) From 298916fe3843e491cf5fd8c04f97dfaf4a56dacd Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Wed, 23 Sep 2026 12:50:17 -0700 Subject: [PATCH 34/36] test: cover concurrent CBOR codec reuse --- gems/smithy-cbor/spec/smithy-cbor/codec_spec.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/gems/smithy-cbor/spec/smithy-cbor/codec_spec.rb b/gems/smithy-cbor/spec/smithy-cbor/codec_spec.rb index 9d1d044e1..dc53acef9 100644 --- a/gems/smithy-cbor/spec/smithy-cbor/codec_spec.rb +++ b/gems/smithy-cbor/spec/smithy-cbor/codec_spec.rb @@ -35,6 +35,17 @@ module Cbor expect(first.to_h).to eq(string: 'first') expect(second.to_h).to eq(integer: 123) end + + it 'supports concurrent builds on the same codec instance' do + codec = described_class.new + builds = 20.times.map do |i| + Thread.new { codec.build(structure_shape, string: "value-#{i}") } + end + + expect(builds.map { |build| Cbor.decode(build.value) }).to eq( + 20.times.map { |i| { 'string' => "value-#{i}" } } + ) + end end end end From 1d4a94f18b04e11ba6cd7fec792f753b7792a347 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Wed, 23 Sep 2026 12:59:16 -0700 Subject: [PATCH 35/36] fix: preserve member block arguments --- gems/smithy-schema/lib/smithy-schema/extension.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gems/smithy-schema/lib/smithy-schema/extension.rb b/gems/smithy-schema/lib/smithy-schema/extension.rb index 2bb0cd91a..6c8dc8e27 100644 --- a/gems/smithy-schema/lib/smithy-schema/extension.rb +++ b/gems/smithy-schema/lib/smithy-schema/extension.rb @@ -132,8 +132,10 @@ def timestamp_format(shape) # Iterates modeled members with separate Ruby name and member-shape # arguments. With no block, returns the underlying enumerator. - def each_member(shape, &) - shape.members.each(&) + def each_member(shape, &block) + return shape.members.each unless block + + shape.members.each { |name, member| block.call(name, member) } end # Returns whether a collection may retain nil values. From bb104363f990c42730ad24da88bedd7f9860bea6 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Wed, 23 Sep 2026 13:43:48 -0700 Subject: [PATCH 36/36] docs: add private extension examples --- .../lib/smithy-client/http_extension.rb | 20 ++++ gems/smithy-json/lib/smithy-json/extension.rb | 16 +++ .../lib/smithy-schema/extension.rb | 99 +++++++++++++++++++ gems/smithy-xml/lib/smithy-xml/extension.rb | 55 +++++++++++ 4 files changed, 190 insertions(+) diff --git a/gems/smithy-client/lib/smithy-client/http_extension.rb b/gems/smithy-client/lib/smithy-client/http_extension.rb index 4db067036..66b2bf9aa 100644 --- a/gems/smithy-client/lib/smithy-client/http_extension.rb +++ b/gems/smithy-client/lib/smithy-client/http_extension.rb @@ -29,20 +29,40 @@ module HttpExtension }.freeze class << self + # Returns the HTTP method. + # + # Example: + # HttpExtension.http_method(operation) + # # => 'GET' def http_method(operation) operation[:http_method] || resolve_operation(operation, :http_method) end + # Returns the HTTP path without its static query string. + # + # Example: + # HttpExtension.path(operation) + # # => '/items/{id}' def path(operation) operation[:http_path] || resolve_operation(operation, :http_path) end + # Returns the static query string from the HTTP URI. + # + # Example: + # HttpExtension.static_query(operation) + # # => 'version=1' def static_query(operation) operation.fetch_metadata(:http_static_query) do resolve_operation(operation, :http_static_query) end end + # Returns the modeled HTTP response code. + # + # Example: + # HttpExtension.response_code(operation) + # # => 200 def response_code(operation) operation[:http_response_code] || resolve_operation(operation, :http_response_code) end diff --git a/gems/smithy-json/lib/smithy-json/extension.rb b/gems/smithy-json/lib/smithy-json/extension.rb index 3d52498d5..96e48c694 100644 --- a/gems/smithy-json/lib/smithy-json/extension.rb +++ b/gems/smithy-json/lib/smithy-json/extension.rb @@ -11,16 +11,28 @@ module Json module Extension class << self # Returns the JSON parse lookup index cached on a structure or union. + # + # Example: + # Extension.wire_index(shape)['wireName'] + # # => [:ruby_name, member] def wire_index(shape) shape[:json_wire_index] || resolve_indexes(shape, :json_wire_index) end # Returns the JSON build lookup index cached on a structure or union. + # + # Example: + # Extension.member_index(shape)[:ruby_name] + # # => ['wireName', member] def member_index(shape) shape[:json_member_index] || resolve_indexes(shape, :json_member_index) end # Returns the effective JSON member name. + # + # Example: + # Extension.wire_name(member) + # # => 'wireName' def wire_name(member) member.fetch_metadata(:json_name) do member.traits['smithy.api#jsonName'] || member.name @@ -28,6 +40,10 @@ def wire_name(member) end # Returns the resolved timestamp format for JSON serialization. + # + # Example: + # Extension.timestamp_format(member) + # # => 'epoch-seconds' def timestamp_format(shape) Schema::Extension.timestamp_format(shape) end diff --git a/gems/smithy-schema/lib/smithy-schema/extension.rb b/gems/smithy-schema/lib/smithy-schema/extension.rb index 6c8dc8e27..acae91995 100644 --- a/gems/smithy-schema/lib/smithy-schema/extension.rb +++ b/gems/smithy-schema/lib/smithy-schema/extension.rb @@ -13,17 +13,29 @@ class << self # Returns the modeled wire-name lookup used by existing serde # consumers. The index maps modeled member name to # [ruby_member_name, member_shape]. + # + # Example: + # Extension.wire_index(shape)['ModeledName'] + # # => [:ruby_name, member] def wire_index(shape) shape[:schema_wire_index] || resolve_aggregate(shape, :schema_wire_index) end # Returns the canonical build lookup index. The index maps Ruby member # name to [modeled_member_name, member_shape]. + # + # Example: + # Extension.member_index(shape)[:ruby_name] + # # => ['ModeledName', member] def member_index(shape) shape[:schema_member_index] || resolve_aggregate(shape, :schema_member_index) end # Returns the modeled media type, when present. + # + # Example: + # Extension.media_type(shape) + # # => 'application/json' def media_type(shape) shape.fetch_metadata(:schema_media_type) do shape.traits['smithy.api#mediaType'] @@ -31,6 +43,10 @@ def media_type(shape) end # Returns whether the sensitive trait is present. + # + # Example: + # Extension.sensitive?(shape) + # # => true def sensitive?(shape) shape.fetch_metadata(:schema_sensitive) do shape.traits.key?('smithy.api#sensitive') @@ -38,6 +54,10 @@ def sensitive?(shape) end # Returns whether the streaming trait is present. + # + # Example: + # Extension.streaming?(shape) + # # => true def streaming?(shape) shape.fetch_metadata(:schema_streaming) do shape.traits.key?('smithy.api#streaming') @@ -45,42 +65,76 @@ def streaming?(shape) end # Returns whether the requires-length trait is present. + # + # Example: + # Extension.requires_length?(shape) + # # => true def requires_length?(shape) shape.fetch_metadata(:schema_requires_length) do shape.traits.key?('smithy.api#requiresLength') end end + # Returns the endpoint host prefix. + # + # Example: + # Extension.endpoint_host_prefix(operation) + # # => '{account_id}.example.com' def endpoint_host_prefix(operation) operation.fetch_metadata(:schema_endpoint_host_prefix) do resolve_endpoint(operation, :schema_endpoint_host_prefix) end end + # Returns the compiled endpoint host-prefix plan. + # + # Example: + # Extension.endpoint_host_prefix_plan(operation) + # # => [:account_id, '.example.com'] def endpoint_host_prefix_plan(operation) operation.fetch_metadata(:schema_endpoint_host_prefix_plan) do resolve_endpoint(operation, :schema_endpoint_host_prefix_plan) end end + # Returns the supported request-compression encodings. + # + # Example: + # Extension.request_compression_encodings(operation) + # # => ['gzip'] def request_compression_encodings(operation) operation.fetch_metadata(:schema_request_compression_encodings) do operation.traits.dig('smithy.api#requestCompression', 'encodings') end end + # Returns whether an HTTP checksum is required. + # + # Example: + # Extension.checksum_required?(operation) + # # => true def checksum_required?(operation) operation.fetch_metadata(:schema_checksum_required) do operation.traits.key?('smithy.api#httpChecksumRequired') end end + # Returns whether an operation uses long polling. + # + # Example: + # Extension.long_polling?(operation) + # # => true def long_polling?(operation) operation.fetch_metadata(:schema_long_polling) do operation.traits.key?('smithy.api#longPoll') end end + # Returns whether an operation uses an unsigned payload. + # + # Example: + # Extension.unsigned_payload?(operation) + # # => true def unsigned_payload?(operation) operation.fetch_metadata(:schema_unsigned_payload) do operation.traits.key?('aws.auth#unsignedPayload') @@ -88,36 +142,70 @@ def unsigned_payload?(operation) end # Returns operation errors indexed by target shape name. + # + # Example: + # Extension.error_index(operation)['ExampleError'] + # # => error_member def error_index(operation) operation[:schema_error_index] ||= build_error_index(operation) end + # Returns required members by Ruby member name. + # + # Example: + # Extension.required_members(shape) + # # => [:name] def required_members(shape) shape[:schema_required_members] || resolve_aggregate(shape, :schema_required_members) end + # Returns host labels indexed by modeled member name. + # + # Example: + # Extension.host_label_index(shape) + # # => { 'AccountId' => :account_id } def host_label_index(shape) shape[:schema_host_label_index] || resolve_aggregate(shape, :schema_host_label_index) end + # Returns the idempotency-token member name. + # + # Example: + # Extension.idempotency_token_member(shape) + # # => :client_token def idempotency_token_member(shape) shape.fetch_metadata(:schema_idempotency_token_member) do resolve_aggregate(shape, :schema_idempotency_token_member) end end + # Returns the streaming member. + # + # Example: + # Extension.streaming_member(shape) + # # => member def streaming_member(shape) shape.fetch_metadata(:schema_streaming_member) do resolve_aggregate(shape, :schema_streaming_member) end end + # Returns the streaming member when its length is unknown. + # + # Example: + # Extension.streaming_member_unknown_length(shape) + # # => member def streaming_member_unknown_length(shape) shape.fetch_metadata(:schema_streaming_member_unknown_length) do resolve_aggregate(shape, :schema_streaming_member_unknown_length) end end + # Returns the event-stream member. + # + # Example: + # Extension.event_stream_member(shape) + # # => member def event_stream_member(shape) shape.fetch_metadata(:schema_event_stream_member) do resolve_aggregate(shape, :schema_event_stream_member) @@ -126,12 +214,19 @@ def event_stream_member(shape) # 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) shape[:schema_timestamp_format] ||= resolve_timestamp_format(shape) 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 @@ -139,6 +234,10 @@ def each_member(shape, &block) end # Returns whether a collection may retain nil values. + # + # Example: + # Extension.sparse?(list) + # # => true def sparse?(shape) shape.fetch_metadata(:schema_sparse) do shape.traits.key?('smithy.api#sparse') diff --git a/gems/smithy-xml/lib/smithy-xml/extension.rb b/gems/smithy-xml/lib/smithy-xml/extension.rb index 10bf65c7c..f86a548c3 100644 --- a/gems/smithy-xml/lib/smithy-xml/extension.rb +++ b/gems/smithy-xml/lib/smithy-xml/extension.rb @@ -11,6 +11,10 @@ module Xml module Extension class << self # Returns the XML wrapper or structure name. + # + # Example: + # Extension.structure_name(shape) + # # => 'Example' def structure_name(shape) shape.fetch_metadata(:xml_structure_name) do resolve_structure_name(shape) @@ -18,6 +22,10 @@ def structure_name(shape) end # Returns whether the XML value is flattened. + # + # Example: + # Extension.flattened?(member) + # # => true def flattened?(shape) shape.fetch_metadata(:xml_flattened) do shape.traits.key?('smithy.api#xmlFlattened') @@ -25,6 +33,10 @@ def flattened?(shape) end # Returns the parser frame class for the shape. + # + # Example: + # Extension.frame_class(member) + # # => Parser::StructureFrame def frame_class(shape) shape.fetch_metadata(:xml_frame_class) do frame_class_for(shape.target, flattened?(shape)) @@ -32,41 +44,84 @@ def frame_class(shape) end # Returns the resolved XML member name. + # + # Example: + # Extension.wire_name(member) + # # => 'ExampleName' def wire_name(member) member[:xml_wire_name] ||= member.traits['smithy.api#xmlName'] || member.name end # Returns XML members partitioned into attributes and elements. + # + # Example: + # Extension.members(shape) + # # => { attributes: [...], elements: [...] } def members(shape) shape[:xml_members] || resolve_members(shape, :members) end + # Returns XML attribute members. + # + # Example: + # Extension.attribute_members(shape) + # # => [[:id, 'id', member]] def attribute_members(shape) shape[:xml_attribute_members] || resolve_members(shape, :attributes) end + # Returns XML element members. + # + # Example: + # Extension.element_members(shape) + # # => [[:name, 'Name', member]] def element_members(shape) shape[:xml_element_members] || resolve_members(shape, :elements) end + # Returns XML members indexed by wire name. + # + # Example: + # Extension.member_index(shape)['Name'] + # # => [:name, member] def member_index(shape) shape[:xml_member_index] || resolve_members(shape, :index) end + # Returns XML namespace attributes. + # + # Example: + # Extension.namespace_attrs(shape) + # # => { 'xmlns' => 'https://example.com' } def namespace_attrs(shape) shape[:xml_namespace_attrs] ||= build_namespace_attrs(shape, shape.target) end + # Returns the resolved key and value parts for an XML map. + # + # Example: + # Extension.map_parts(member) + # # => ['key', key_member, 'value', value_member] def map_parts(shape) shape.fetch_metadata(:xml_map_parts) do build_map_parts(shape.target) end end + # Returns the resolved timestamp format. + # + # Example: + # Extension.timestamp_format(member) + # # => 'date-time' def timestamp_format(shape) Schema::Extension.timestamp_format(shape) end + # Returns whether a collection may include nil values. + # + # Example: + # Extension.sparse?(list) + # # => true def sparse?(shape) Schema::Extension.sparse?(shape) end