From d33fcdfa691322eea0ad119fcf6a986859085485 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Thu, 6 Mar 2025 12:12:16 -0800 Subject: [PATCH 01/54] Add type registry prototype class --- .../lib/smithy-schema/type_registry.rb | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 gems/smithy-schema/lib/smithy-schema/type_registry.rb diff --git a/gems/smithy-schema/lib/smithy-schema/type_registry.rb b/gems/smithy-schema/lib/smithy-schema/type_registry.rb new file mode 100644 index 000000000..18f90cb6f --- /dev/null +++ b/gems/smithy-schema/lib/smithy-schema/type_registry.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Smithy + module Schema + # Registry that contains a map of Smithy shape ID to its shape representation + # TODO: Implement a method that takes a document and deserializes + class TypeRegistry + def initialize + @registry = {} + end + + # @return [Hash] + attr_accessor :registry + + # @param [Array] shapes + def register(*shapes) + raise ArgumentError, 'Expected an array of Shapes' unless shapes.all?(Shape) + + shapes.each do |s| + next if s.id.nil? + + @registry[s.id] = s + end + end + + # @param [String] shape_id + # @return [Boolean] + def shape?(shape_id) + @registry.key?(shape_id) + end + + # @param [String] shape_id + # @return [Shape] + def shape(shape_id) + @registry[shape_id] + end + + class << self + + # TODO: Need thoughts on... + # * Smithy-Java only allows to compose 2 registries at a time, + # * Do we follow suit or allow unlimited number of registry to compose? + # @param [Array] + # @return [TypeRegistry] + def compose(*type_registries) + raise ArgumentError, 'Expected an array of TypeRegistries' unless type_registries.all?(self) + + new_type_registry = new + new_type_registry.registry = + type_registries.each_with_object({}) { |r, h| h.merge!(r.registry) } + new_type_registry + end + end + end + end +end From 5ff40cc2ca108449121653612a25ebb9f2cb678b Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Thu, 6 Mar 2025 13:41:57 -0800 Subject: [PATCH 02/54] Add type registry to codegenerated schema --- gems/smithy-schema/lib/smithy-schema.rb | 1 + .../lib/smithy-schema/type_registry.rb | 14 +++++++------- gems/smithy/lib/smithy/templates/client/schema.erb | 4 +++- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/gems/smithy-schema/lib/smithy-schema.rb b/gems/smithy-schema/lib/smithy-schema.rb index d41bdf49d..4dc819f22 100644 --- a/gems/smithy-schema/lib/smithy-schema.rb +++ b/gems/smithy-schema/lib/smithy-schema.rb @@ -2,6 +2,7 @@ require_relative 'smithy-schema/shapes' require_relative 'smithy-schema/structure' +require_relative 'smithy-schema/type_registry' require_relative 'smithy-schema/union' module Smithy diff --git a/gems/smithy-schema/lib/smithy-schema/type_registry.rb b/gems/smithy-schema/lib/smithy-schema/type_registry.rb index 18f90cb6f..a15aaa1f9 100644 --- a/gems/smithy-schema/lib/smithy-schema/type_registry.rb +++ b/gems/smithy-schema/lib/smithy-schema/type_registry.rb @@ -4,17 +4,18 @@ module Smithy module Schema # Registry that contains a map of Smithy shape ID to its shape representation # TODO: Implement a method that takes a document and deserializes + # Do we include operation shapes in this registry? class TypeRegistry def initialize @registry = {} end - # @return [Hash] + # @return [Hash] attr_accessor :registry - # @param [Array] shapes + # @param [Array] shapes def register(*shapes) - raise ArgumentError, 'Expected an array of Shapes' unless shapes.all?(Shape) + raise ArgumentError, 'Expected an array of Shapes' unless shapes.all?(Shapes::Shape) shapes.each do |s| next if s.id.nil? @@ -30,16 +31,15 @@ def shape?(shape_id) end # @param [String] shape_id - # @return [Shape] + # @return [Shapes::Shape] def shape(shape_id) @registry[shape_id] end class << self - # TODO: Need thoughts on... - # * Smithy-Java only allows to compose 2 registries at a time, - # * Do we follow suit or allow unlimited number of registry to compose? + # * Smithy-Java only allows to compose 2 type registries at a time. + # Do we follow suit or allow unlimited number of registry to compose? # @param [Array] # @return [TypeRegistry] def compose(*type_registries) diff --git a/gems/smithy/lib/smithy/templates/client/schema.erb b/gems/smithy/lib/smithy/templates/client/schema.erb index 525618985..1dfb41455 100644 --- a/gems/smithy/lib/smithy/templates/client/schema.erb +++ b/gems/smithy/lib/smithy/templates/client/schema.erb @@ -21,8 +21,8 @@ module <%= module_name %> <% if shape.typed -%> <%= shape.name %>.type = Types::<%= shape.name %> <% end -%> - <% end -%> + SERVICE = ServiceShape.new do |service| service.id = "<%= service_shape.id %>" service.name = "<%= service_shape.name %>" @@ -41,5 +41,7 @@ module <%= module_name %> end) <% end -%> end + TYPE_REGISTRY = Smithy::Schema::TypeRegistry.new + TYPE_REGISTRY.register(<% shapes.each do |s| %><%= s.name %>, <% end %>) end end From 66a62858724ceecbd8a828971e1c06bd2089cef0 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Thu, 6 Mar 2025 13:42:21 -0800 Subject: [PATCH 03/54] Update projections --- projections/shapes/lib/shapes/schema.rb | 68 +++++++++---------- .../spec/shapes/endpoint_provider_spec.rb | 6 +- projections/weather/lib/weather/schema.rb | 12 +--- 3 files changed, 37 insertions(+), 49 deletions(-) diff --git a/projections/shapes/lib/shapes/schema.rb b/projections/shapes/lib/shapes/schema.rb index 864d0b0f4..ad7db01c9 100644 --- a/projections/shapes/lib/shapes/schema.rb +++ b/projections/shapes/lib/shapes/schema.rb @@ -7,36 +7,32 @@ module ShapeService module Schema include Smithy::Schema::Shapes - BigDecimal = BigDecimalShape.new(id: 'smithy.ruby.tests#BigDecimal', traits: {"smithy.ruby.tests#shape" => {}}) - BigInteger = IntegerShape.new(id: 'smithy.ruby.tests#BigInteger', traits: {"smithy.ruby.tests#shape" => {}}) - Blob = BlobShape.new(id: 'smithy.ruby.tests#Blob', traits: {"smithy.ruby.tests#shape" => {}}) - Boolean = BooleanShape.new(id: 'smithy.ruby.tests#Boolean', traits: {"smithy.ruby.tests#shape" => {}}) - Byte = IntegerShape.new(id: 'smithy.ruby.tests#Byte', traits: {"smithy.ruby.tests#shape" => {}}) - Document = DocumentShape.new(id: 'smithy.ruby.tests#Document', traits: {"smithy.ruby.tests#shape" => {}}) - Double = FloatShape.new(id: 'smithy.ruby.tests#Double', traits: {"smithy.ruby.tests#shape" => {}}) - Enum = EnumShape.new(id: 'smithy.ruby.tests#Enum', traits: {"smithy.ruby.tests#shape" => {}}) - Float = FloatShape.new(id: 'smithy.ruby.tests#Float', traits: {"smithy.ruby.tests#shape" => {}}) - IntEnum = IntEnumShape.new(id: 'smithy.ruby.tests#IntEnum', traits: {"smithy.ruby.tests#shape" => {}}) - Integer = IntegerShape.new(id: 'smithy.ruby.tests#Integer', traits: {"smithy.ruby.tests#shape" => {}}) - List = ListShape.new(id: 'smithy.ruby.tests#List', traits: {"smithy.ruby.tests#shape" => {}}) - Long = IntegerShape.new(id: 'smithy.ruby.tests#Long', traits: {"smithy.ruby.tests#shape" => {}}) - Map = MapShape.new(id: 'smithy.ruby.tests#Map', traits: {"smithy.ruby.tests#shape" => {}}) + BigDecimal = BigDecimalShape.new(id: 'smithy.ruby.tests#BigDecimal', traits: {"smithy.ruby.tests#shape"=>{}}) + BigInteger = IntegerShape.new(id: 'smithy.ruby.tests#BigInteger', traits: {"smithy.ruby.tests#shape"=>{}}) + Blob = BlobShape.new(id: 'smithy.ruby.tests#Blob', traits: {"smithy.ruby.tests#shape"=>{}}) + Boolean = BooleanShape.new(id: 'smithy.ruby.tests#Boolean', traits: {"smithy.ruby.tests#shape"=>{}}) + Byte = IntegerShape.new(id: 'smithy.ruby.tests#Byte', traits: {"smithy.ruby.tests#shape"=>{}}) + Document = DocumentShape.new(id: 'smithy.ruby.tests#Document', traits: {"smithy.ruby.tests#shape"=>{}}) + Double = FloatShape.new(id: 'smithy.ruby.tests#Double', traits: {"smithy.ruby.tests#shape"=>{}}) + Enum = EnumShape.new(id: 'smithy.ruby.tests#Enum', traits: {"smithy.ruby.tests#shape"=>{}}) + Float = FloatShape.new(id: 'smithy.ruby.tests#Float', traits: {"smithy.ruby.tests#shape"=>{}}) + IntEnum = IntEnumShape.new(id: 'smithy.ruby.tests#IntEnum', traits: {"smithy.ruby.tests#shape"=>{}}) + Integer = IntegerShape.new(id: 'smithy.ruby.tests#Integer', traits: {"smithy.ruby.tests#shape"=>{}}) + List = ListShape.new(id: 'smithy.ruby.tests#List', traits: {"smithy.ruby.tests#shape"=>{}}) + Long = IntegerShape.new(id: 'smithy.ruby.tests#Long', traits: {"smithy.ruby.tests#shape"=>{}}) + Map = MapShape.new(id: 'smithy.ruby.tests#Map', traits: {"smithy.ruby.tests#shape"=>{}}) OperationInputOutput = StructureShape.new(id: 'smithy.ruby.tests#OperationInputOutput') - Short = IntegerShape.new(id: 'smithy.ruby.tests#Short', traits: {"smithy.ruby.tests#shape" => {}}) - String = StringShape.new(id: 'smithy.ruby.tests#String', traits: {"smithy.ruby.tests#shape" => {}}) - Structure = StructureShape.new(id: 'smithy.ruby.tests#Structure', traits: {"smithy.ruby.tests#shape" => {}}) - Timestamp = TimestampShape.new(id: 'smithy.ruby.tests#Timestamp', traits: {"smithy.ruby.tests#shape" => {}}) - Union = UnionShape.new(id: 'smithy.ruby.tests#Union', traits: {"smithy.ruby.tests#shape" => {}}) - - Enum.add_member(:foo, 'FOO', Prelude::Unit, traits: {"smithy.api#enumValue" => "bar"}) - - IntEnum.add_member(:baz, 'BAZ', Prelude::Unit, traits: {"smithy.api#enumValue" => 1}) - - List.set_member(String, traits: {"smithy.ruby.tests#shape" => {}}) - - Map.set_key(String, traits: {"smithy.ruby.tests#shape" => {}}) - Map.set_value(String, traits: {"smithy.ruby.tests#shape" => {}}) + Short = IntegerShape.new(id: 'smithy.ruby.tests#Short', traits: {"smithy.ruby.tests#shape"=>{}}) + String = StringShape.new(id: 'smithy.ruby.tests#String', traits: {"smithy.ruby.tests#shape"=>{}}) + Structure = StructureShape.new(id: 'smithy.ruby.tests#Structure', traits: {"smithy.ruby.tests#shape"=>{}}) + Timestamp = TimestampShape.new(id: 'smithy.ruby.tests#Timestamp', traits: {"smithy.ruby.tests#shape"=>{}}) + Union = UnionShape.new(id: 'smithy.ruby.tests#Union', traits: {"smithy.ruby.tests#shape"=>{}}) + Enum.add_member(:foo, 'FOO', Prelude::Unit, traits: {"smithy.api#enumValue"=>"bar"}) + IntEnum.add_member(:baz, 'BAZ', Prelude::Unit, traits: {"smithy.api#enumValue"=>1}) + List.set_member(String, traits: {"smithy.ruby.tests#shape"=>{}}) + Map.set_key(String, traits: {"smithy.ruby.tests#shape"=>{}}) + Map.set_value(String, traits: {"smithy.ruby.tests#shape"=>{}}) OperationInputOutput.add_member(:blob, 'blob', Blob) OperationInputOutput.add_member(:boolean, 'boolean', Boolean) OperationInputOutput.add_member(:string, 'string', String) @@ -57,13 +53,11 @@ module Schema OperationInputOutput.add_member(:structure, 'structure', Structure) OperationInputOutput.add_member(:union, 'union', Union) OperationInputOutput.type = Types::OperationInputOutput - - Structure.add_member(:member, 'member', String, traits: {"smithy.ruby.tests#shape" => {}}) + Structure.add_member(:member, 'member', String, traits: {"smithy.ruby.tests#shape"=>{}}) Structure.type = Types::Structure - - Union.add_member(:string, 'string', String, Types::Union::String, traits: {"smithy.ruby.tests#shape" => {}}) - Union.add_member(:structure, 'structure', Structure, Types::Union::Structure, traits: {"smithy.ruby.tests#shape" => {}}) - Union.add_member(:unit, 'unit', Prelude::Unit, Types::Union::Unit, traits: {"smithy.ruby.tests#shape" => {}}) + Union.add_member(:string, 'string', String, Types::Union::String, traits: {"smithy.ruby.tests#shape"=>{}}) + Union.add_member(:structure, 'structure', Structure, Types::Union::Structure, traits: {"smithy.ruby.tests#shape"=>{}}) + Union.add_member(:unit, 'unit', Prelude::Unit, Types::Union::Unit, traits: {"smithy.ruby.tests#shape"=>{}}) Union.add_member(:unknown, 'unknown', Prelude::Unit, Types::Union::Unknown) Union.type = Types::Union @@ -71,14 +65,16 @@ module Schema service.id = "smithy.ruby.tests#ShapeService" service.name = "ShapeService" service.version = "2018-10-31" - service.traits = {"smithy.ruby.tests#shape" => {}} + service.traits = {"smithy.ruby.tests#shape"=>{}} service.add_operation(:operation, OperationShape.new do |operation| operation.id = "smithy.ruby.tests#Operation" operation.name = "Operation" operation.input = OperationInputOutput operation.output = OperationInputOutput - operation.traits = {"smithy.ruby.tests#shape" => {}} + operation.traits = {"smithy.ruby.tests#shape"=>{}} end) end + TYPE_REGISTRY = Smithy::Schema::TypeRegistry.new + TYPE_REGISTRY.register(BigDecimal, BigInteger, Blob, Boolean, Byte, Document, Double, Enum, Float, IntEnum, Integer, List, Long, Map, OperationInputOutput, Short, String, Structure, Timestamp, Union, ) end end diff --git a/projections/shapes/spec/shapes/endpoint_provider_spec.rb b/projections/shapes/spec/shapes/endpoint_provider_spec.rb index 7eb681e11..26f6bfd8e 100644 --- a/projections/shapes/spec/shapes/endpoint_provider_spec.rb +++ b/projections/shapes/spec/shapes/endpoint_provider_spec.rb @@ -19,11 +19,11 @@ module ShapeService context "Endpoint set" do let(:expected) do - {"endpoint" => {"url" => "https://example.com"}} + {"endpoint"=>{"url"=>"https://example.com"}} end it 'produces the expected output from the EndpointProvider' do - params = EndpointParameters.new(**{endpoint: "https://example.com"}) + params = EndpointParameters.new(**{:endpoint=>"https://example.com"}) endpoint = subject.resolve_endpoint(params) expect(endpoint.uri).to eq(expected['endpoint']['url']) expect(endpoint.headers).to eq(expected['endpoint']['headers'] || {}) @@ -34,7 +34,7 @@ module ShapeService context "Endpoint not set" do let(:expected) do - {"error" => "Endpoint is not set - you must configure an endpoint."} + {"error"=>"Endpoint is not set - you must configure an endpoint."} end it 'produces the expected output from the EndpointProvider' do diff --git a/projections/weather/lib/weather/schema.rb b/projections/weather/lib/weather/schema.rb index 5c034c2eb..534ad47c3 100644 --- a/projections/weather/lib/weather/schema.rb +++ b/projections/weather/lib/weather/schema.rb @@ -23,37 +23,27 @@ module Schema CityCoordinates.add_member(:latitude, 'latitude', Prelude::Float, traits: { 'smithy.api#required' => {} }) CityCoordinates.add_member(:longitude, 'longitude', Prelude::Float, traits: { 'smithy.api#required' => {} }) CityCoordinates.type = Types::CityCoordinates - CitySummaries.set_member(CitySummary) - CitySummary.add_member(:city_id, 'cityId', CityId, traits: { 'smithy.api#required' => {} }) CitySummary.add_member(:name, 'name', Prelude::String, traits: { 'smithy.api#required' => {} }) CitySummary.type = Types::CitySummary - GetCityInput.add_member(:city_id, 'cityId', CityId, traits: { 'smithy.api#required' => {} }) GetCityInput.type = Types::GetCityInput - GetCityOutput.add_member(:name, 'name', Prelude::String, traits: { 'smithy.api#notProperty' => {}, 'smithy.api#required' => {} }) GetCityOutput.add_member(:coordinates, 'coordinates', CityCoordinates, traits: { 'smithy.api#required' => {} }) GetCityOutput.type = Types::GetCityOutput - GetCurrentTimeOutput.add_member(:time, 'time', Prelude::Timestamp, traits: { 'smithy.api#required' => {} }) GetCurrentTimeOutput.type = Types::GetCurrentTimeOutput - GetForecastInput.add_member(:city_id, 'cityId', CityId, traits: { 'smithy.api#required' => {} }) GetForecastInput.type = Types::GetForecastInput - GetForecastOutput.add_member(:chance_of_rain, 'chanceOfRain', Prelude::Float) GetForecastOutput.type = Types::GetForecastOutput - ListCitiesInput.add_member(:next_token, 'nextToken', Prelude::String) ListCitiesInput.add_member(:page_size, 'pageSize', Prelude::Integer) ListCitiesInput.type = Types::ListCitiesInput - ListCitiesOutput.add_member(:next_token, 'nextToken', Prelude::String) ListCitiesOutput.add_member(:items, 'items', CitySummaries, traits: { 'smithy.api#required' => {} }) ListCitiesOutput.type = Types::ListCitiesOutput - NoSuchResource.add_member(:resource_type, 'resourceType', Prelude::String, traits: { 'smithy.api#required' => {} }) NoSuchResource.type = Types::NoSuchResource @@ -92,5 +82,7 @@ module Schema operation.traits = { 'smithy.api#paginated' => { 'items' => 'items' }, 'smithy.api#readonly' => {} } end) end + TYPE_REGISTRY = Smithy::Schema::TypeRegistry.new + TYPE_REGISTRY.register(CityCoordinates, CityId, CitySummaries, CitySummary, GetCityInput, GetCityOutput, GetCurrentTimeOutput, GetForecastInput, GetForecastOutput, ListCitiesInput, ListCitiesOutput, NoSuchResource) end end From e6435d5cc11fea52fb1f7d5acf4e6f0381e2ef07 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Thu, 3 Apr 2025 10:56:19 -0700 Subject: [PATCH 04/54] Update requires --- gems/smithy-schema/lib/smithy-schema.rb | 1 + gems/smithy-schema/spec/spec_helper.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/gems/smithy-schema/lib/smithy-schema.rb b/gems/smithy-schema/lib/smithy-schema.rb index b5518b416..55221cf63 100644 --- a/gems/smithy-schema/lib/smithy-schema.rb +++ b/gems/smithy-schema/lib/smithy-schema.rb @@ -2,6 +2,7 @@ require_relative 'smithy-schema/shapes' require_relative 'smithy-schema/structure' +require_relative 'smithy-schema/document' require_relative 'smithy-schema/type_registry' require_relative 'smithy-schema/union' diff --git a/gems/smithy-schema/spec/spec_helper.rb b/gems/smithy-schema/spec/spec_helper.rb index b6e8d700b..2d654eb6a 100644 --- a/gems/smithy-schema/spec/spec_helper.rb +++ b/gems/smithy-schema/spec/spec_helper.rb @@ -4,6 +4,7 @@ SimpleCov.start require 'smithy-schema' +require_relative 'support/schema_helper' # This file was generated by the `rspec --init` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. From 08308270ea13c48f571379356fc06cc1d4af4bb6 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Thu, 3 Apr 2025 11:00:32 -0700 Subject: [PATCH 05/54] Add initial document implementation --- .../lib/smithy-schema/document.rb | 159 +++++++++++++++ .../spec/smithy-schema/document_spec.rb | 189 ++++++++++++++++++ 2 files changed, 348 insertions(+) create mode 100644 gems/smithy-schema/lib/smithy-schema/document.rb create mode 100644 gems/smithy-schema/spec/smithy-schema/document_spec.rb diff --git a/gems/smithy-schema/lib/smithy-schema/document.rb b/gems/smithy-schema/lib/smithy-schema/document.rb new file mode 100644 index 000000000..0544009cc --- /dev/null +++ b/gems/smithy-schema/lib/smithy-schema/document.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true + +require 'json' + +module Smithy + module Schema + # TODO: need to address the following and more + # * documentation + # * some json considerations like timestamp, jsonName trait etc + # * handle union stuffs + class Document + def initialize(data, schema = nil) + @data = format_data(data, schema) # ruby obj to make it easy to work with + @discriminator = schema ? extract_discriminator(data, schema) : nil + end + + attr_reader :data, :discriminator + + # if discriminator is set, add discriminator being generating + def as_json + JSON.generate(@data, allow_nan: true) # do we want to allow this? + end + + def [](key) + return unless @data.is_a?(Hash) && @data.key?(key) + + @data[key] + end + + # expected schema here is a shape that has a type representation + def as_typed(schema) + # ensures that given schema has a type representation and + error_message = 'Invalid schema or document data' + raise ArgumentError, error_message unless valid_schema?(schema) && @data.is_a?(Hash) + + type = schema.type.new + apply_data(schema, @data, type) + end + + private + + def apply_data(schema, data, type = nil) + case schema + when Shapes::StructureShape then apply_structure(schema, data, type) + # when Shapes::UnionShape then union(shape, value, type) + when Shapes::ListShape then apply_list(schema, data) + when Shapes::MapShape then apply_map(schema, data) + else data + end + end + + def apply_structure(schema, data, type) + type = schema.type.new if type.nil? + data.each do |k, v| + next if (name = resolve_member_name(schema, k)).nil? + + type[name] = apply_data(schema.member(name).shape, v) + end + type + end + + def resolve_member_name(schema, key) + return unless schema.name_by_member_name?(key) || schema.member?(key.to_sym) + + schema.name_by_member_name(key) || key.to_sym + end + + def apply_list(schema, data) + data.map do |v| + next if v.nil? + + apply_data(schema.member.shape, v) + end + end + + def apply_map(schema, data) + data.each_with_object({}) do |(k, v), h| + h[k.to_s] = + if v.nil? + nil + else + apply_data(schema.value.shape, v) + end + end + end + + def valid_schema?(schema) + schema.is_a?(Shapes::StructureShape) && !schema.type.nil? + end + + def discriminator?(data) + data.is_a?(Hash) && data.key?('__type') + end + + def format_data(data, schema) + return if data.nil? + + case data + when Smithy::Schema::Structure # indicates that this is a runtime shape + if schema.nil? || !schema.is_a?(Shapes::StructureShape) + raise ArgumentError, 'Unable to convert as document with given schema' + end + + extract_data(schema, data) + else + data + end + end + + # handle timestamp, union, number? + def extract_data(schema, data) + return nil if data.nil? + + case schema + when Shapes::StructureShape then extract_structure(schema, data) + when Shapes::ListShape then extract_list(schema, data) + when Shapes::MapShape then extract_map(schema, data) + when Shapes::BlobShape then extract_blob(data) + else data + end + end + + def extract_structure(schema, data) + data.to_h.each_with_object({}) do |(k, v), o| + next unless schema.member?(k) + + member_shape = schema.member(k) + o[member_shape.name] = extract_data(member_shape.shape, v) + end + end + + def extract_list(schema, data) + data.collect { |value| extract_data(schema.member.shape, value) } + end + + def extract_map(schema, data) + data.each.with_object({}) do |(k, v), h| + h[k.to_s] = extract_data(schema.value.shape, v) + end + end + + def extract_blob(data) + Base64.strict_encode64(data.is_a?(String) ? data : data.read) + end + + def extract_discriminator(data, schema) + return if data.nil? + + if discriminator?(data) + data['__type'] + elsif schema + raise "Expected a structure schema, given #{schema} instead" unless schema.is_a?(Shapes::Shape) + + schema.id + end + end + end + end +end diff --git a/gems/smithy-schema/spec/smithy-schema/document_spec.rb b/gems/smithy-schema/spec/smithy-schema/document_spec.rb new file mode 100644 index 000000000..5189d5bfe --- /dev/null +++ b/gems/smithy-schema/spec/smithy-schema/document_spec.rb @@ -0,0 +1,189 @@ +# frozen_string_literal: true + +require_relative '../spec_helper' + +module Smithy + module Schema + describe Document do + # correct handles integer, blob? union? time? + let(:runtime_shape) do + Struct.new(:string, keyword_init: true) do + include Smithy::Schema::Structure + end + end + + let(:schema_shape) do + string_shape = Shapes::StringShape.new(id: 'smithy.api#String') + shape = Shapes::StructureShape.new(id: 'smithy.ruby.tests#Structure') + shape.add_member(:string, 'stringMember', string_shape) + shape.type = runtime_shape + shape + end + + let(:aggregate_runtime_shape) do + Struct.new( + :string, + :list, + :foo_map, + :structure, + keyword_init: true + ) do + include Smithy::Schema::Structure + end + end + + let(:aggregate_schema_shape) do + string_shape = Shapes::StringShape.new(id: 'smithy.api#String') + list_shape = Shapes::ListShape.new(id: 'smithy.ruby.tests#List') + list_shape.set_member(Shapes::Prelude::String) + map_shape = Shapes::MapShape.new(id: 'smithy.ruby.tests#Map') + map_shape.set_key(Shapes::Prelude::String) + map_shape.set_value(list_shape) + shape = Shapes::StructureShape.new(id: 'smithy.ruby.tests#Structure') + shape.add_member(:string, 'stringMember', string_shape) + shape.add_member(:list, 'listMember', list_shape) + shape.add_member(:foo_map, 'mapMember', map_shape) + shape.add_member(:structure, 'structureMember', shape) + shape.type = aggregate_runtime_shape + shape + end + + context 'untyped document' do + subject { Document.new('foo') } + + describe '#initialize' do + it 'sets given data' do + expect(subject.data).to eq('foo') + end + + it 'defaults discriminator to nil' do + expect(subject.discriminator).to be_nil + end + end + + describe '#[]' do + subject { Document.new({ foo: 'bar' }) } + + it 'returns member value' do + expect(subject[:foo]).to eq('bar') + end + + it 'returns nil when member key is not applicable' do + expect(subject['baz']).to be_nil + end + end + + describe '#discriminator' do + it 'is not set' do + expect(subject.discriminator).to be_nil + end + end + + describe '#as_typed' do + it 'converts document as runtime shape' do + typed_shape = Document.new({ string: 'foo' }).as_typed(schema_shape) + expect(typed_shape).to be_a(runtime_shape) + expect(typed_shape[:string]).to eq('foo') + end + + it 'raises when invalid schema is given' do + invalid_schema = Shapes::StringShape.new(id: 'smithy.api#String') + expect do + subject.as_typed(invalid_schema) + end.to raise_error(ArgumentError) + end + + it 'raises when document cannot be converted' do + expect do + subject.as_typed(schema_shape) + end.to raise_error(ArgumentError) + end + end + + describe '#as_json' do + # TODO + end + end + + context 'typed document' do + let(:typed_shape) do + aggregate_runtime_shape.new( + string: 'foo', + list: %w[Item1 Item2], + foo_map: { foo: ['Thing'] }, + structure: { list: ['AnotherThing'] } + ) + end + + subject { Document.new(typed_shape, aggregate_schema_shape) } + + context 'when runtime shape is the input' do + describe '#initialize' do + it 'sets data' do + expected_data = { + 'stringMember' => 'foo', + 'listMember' => %w[Item1 Item2], + 'mapMember' => { 'foo' => ['Thing'] }, + 'structureMember' => { 'listMember' => ['AnotherThing'] } + } + expect(subject.data).to eq(expected_data) + end + + it 'sets discriminator' do + expect(subject.discriminator).to be(schema_shape.id) + end + + it 'raises when no schema is given' do + expect do + Document.new(typed_shape) + end.to raise_error(ArgumentError) + end + + it 'raises when unable to deconstruct data with schema' do + invalid_schema = Shapes::StringShape.new(id: 'smithy.api#String') + expect do + Document.new(typed_shape, invalid_schema) + end.to raise_error(ArgumentError) + end + end + + describe '#[]' do + it 'returns member value' do + expect(subject['stringMember']).to eq('foo') + end + + it 'returns nil when member key is not applicable' do + expect(subject['someInvalidMember']).to be_nil + end + end + + describe '#discriminator' do + it 'is not nil' do + expect(subject.discriminator).not_to be_nil + end + end + + describe '#as_typed' do + it 'converts document as a runtime shape' do + typed_shape = subject.as_typed(schema_shape) + expect(typed_shape).to be_a(runtime_shape) + expect(typed_shape[:string]).to eq('foo') + end + + it 'raises when unable to convert as runtime shape' do + # TODO + end + end + + describe '#as_json' do + # TODO + end + end + + context 'when json is given' do + # TODO + end + end + end + end +end From a61318f525e0fc667739558029144e79e4b8bc23 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 7 Apr 2025 13:18:26 -0700 Subject: [PATCH 06/54] Update to include cbor --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 92dc2a0d4..9818749e8 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ bundle exec smithy-ruby smith client --gem-name weather --gem-version 1.0.0 --de ### IRB IRB on `weather` gem: ``` -irb -I projections/weather/lib -I gems/smithy-client/lib -I gems/smithy-schema/lib -r weather +irb -I projections/weather/lib -I gems/smithy-client/lib -I gems/smithy-schema/lib -I gems/smithy-cbor/lib -r weather ``` Create a Weather client: From 4edfae3b50187d50040527a446ffb381437230d4 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 7 Apr 2025 13:18:53 -0700 Subject: [PATCH 07/54] Expand on typed docs --- .../lib/smithy-schema/document.rb | 125 ++-------------- .../lib/smithy-schema/documents.rb | 139 ++++++++++++++++++ .../spec/smithy-schema/document_spec.rb | 86 +++++------ 3 files changed, 188 insertions(+), 162 deletions(-) create mode 100644 gems/smithy-schema/lib/smithy-schema/documents.rb diff --git a/gems/smithy-schema/lib/smithy-schema/document.rb b/gems/smithy-schema/lib/smithy-schema/document.rb index 0544009cc..1392d6915 100644 --- a/gems/smithy-schema/lib/smithy-schema/document.rb +++ b/gems/smithy-schema/lib/smithy-schema/document.rb @@ -1,25 +1,20 @@ # frozen_string_literal: true -require 'json' +require_relative 'documents' module Smithy module Schema # TODO: need to address the following and more # * documentation - # * some json considerations like timestamp, jsonName trait etc - # * handle union stuffs class Document + include Documents def initialize(data, schema = nil) - @data = format_data(data, schema) # ruby obj to make it easy to work with - @discriminator = schema ? extract_discriminator(data, schema) : nil + @data = format_data(data, schema) + @discriminator = Extractor.discriminator(data, schema) + @schema = schema end - attr_reader :data, :discriminator - - # if discriminator is set, add discriminator being generating - def as_json - JSON.generate(@data, allow_nan: true) # do we want to allow this? - end + attr_reader :data, :discriminator, :schema def [](key) return unless @data.is_a?(Hash) && @data.key?(key) @@ -27,67 +22,16 @@ def [](key) @data[key] end - # expected schema here is a shape that has a type representation def as_typed(schema) - # ensures that given schema has a type representation and error_message = 'Invalid schema or document data' raise ArgumentError, error_message unless valid_schema?(schema) && @data.is_a?(Hash) type = schema.type.new - apply_data(schema, @data, type) + Applier.apply(schema, @data, type) end private - def apply_data(schema, data, type = nil) - case schema - when Shapes::StructureShape then apply_structure(schema, data, type) - # when Shapes::UnionShape then union(shape, value, type) - when Shapes::ListShape then apply_list(schema, data) - when Shapes::MapShape then apply_map(schema, data) - else data - end - end - - def apply_structure(schema, data, type) - type = schema.type.new if type.nil? - data.each do |k, v| - next if (name = resolve_member_name(schema, k)).nil? - - type[name] = apply_data(schema.member(name).shape, v) - end - type - end - - def resolve_member_name(schema, key) - return unless schema.name_by_member_name?(key) || schema.member?(key.to_sym) - - schema.name_by_member_name(key) || key.to_sym - end - - def apply_list(schema, data) - data.map do |v| - next if v.nil? - - apply_data(schema.member.shape, v) - end - end - - def apply_map(schema, data) - data.each_with_object({}) do |(k, v), h| - h[k.to_s] = - if v.nil? - nil - else - apply_data(schema.value.shape, v) - end - end - end - - def valid_schema?(schema) - schema.is_a?(Shapes::StructureShape) && !schema.type.nil? - end - def discriminator?(data) data.is_a?(Hash) && data.key?('__type') end @@ -96,63 +40,20 @@ def format_data(data, schema) return if data.nil? case data - when Smithy::Schema::Structure # indicates that this is a runtime shape + when Smithy::Schema::Structure if schema.nil? || !schema.is_a?(Shapes::StructureShape) raise ArgumentError, 'Unable to convert as document with given schema' end - extract_data(schema, data) + Extractor.extract(schema, data) else - data - end - end - - # handle timestamp, union, number? - def extract_data(schema, data) - return nil if data.nil? - - case schema - when Shapes::StructureShape then extract_structure(schema, data) - when Shapes::ListShape then extract_list(schema, data) - when Shapes::MapShape then extract_map(schema, data) - when Shapes::BlobShape then extract_blob(data) - else data - end - end - - def extract_structure(schema, data) - data.to_h.each_with_object({}) do |(k, v), o| - next unless schema.member?(k) - - member_shape = schema.member(k) - o[member_shape.name] = extract_data(member_shape.shape, v) - end - end - - def extract_list(schema, data) - data.collect { |value| extract_data(schema.member.shape, value) } - end - - def extract_map(schema, data) - data.each.with_object({}) do |(k, v), h| - h[k.to_s] = extract_data(schema.value.shape, v) + data = data.except('__type') if discriminator?(data) + data # TODO: add some validation if schema exists end end - def extract_blob(data) - Base64.strict_encode64(data.is_a?(String) ? data : data.read) - end - - def extract_discriminator(data, schema) - return if data.nil? - - if discriminator?(data) - data['__type'] - elsif schema - raise "Expected a structure schema, given #{schema} instead" unless schema.is_a?(Shapes::Shape) - - schema.id - end + def valid_schema?(schema) + schema.is_a?(Shapes::StructureShape) && !schema.type.nil? end end end diff --git a/gems/smithy-schema/lib/smithy-schema/documents.rb b/gems/smithy-schema/lib/smithy-schema/documents.rb new file mode 100644 index 000000000..d6212710b --- /dev/null +++ b/gems/smithy-schema/lib/smithy-schema/documents.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +module Smithy + module Schema + module Documents + # Contains methods to extract given data as document data + module Extractor + class << self + def extract(schema, data) + return nil if data.nil? + + case schema + when Shapes::StructureShape then extract_structure(schema, data) + when Shapes::UnionShape then extract_union(schema, data) + when Shapes::ListShape then extract_list(schema, data) + when Shapes::MapShape then extract_map(schema, data) + else data + end + end + + def extract_structure(schema, data) + data.to_h.each_with_object({}) do |(k, v), o| + next unless schema.member?(k) + + o[k] = extract(schema.member(k).shape, v) + end + end + + def extract_union(schema, data) + output = {} + if data.is_a?(Schema::Union) + member_shape = schema.member_by_type(data.class) + output[member_shape.name] = extract(member_shape.shape, data).value + else + key, value = data.first + if schema.member?(key) + member_shape = schema.member(key) + output[member_shape.name] = extract(member_shape.shape, value) + end + end + output + end + + def extract_list(schema, data) + data.collect { |v| extract(schema.member.shape, v) } + end + + def extract_map(schema, data) + data.each.with_object({}) do |(k, v), h| + h[k] = extract(schema.value.shape, v) + end + end + + def discriminator(data, schema) + return if data.nil? + + if discriminator?(data) + data['__type'] + elsif schema + unless schema.is_a?(Shapes::StructureShape) + raise "Expected a structure schema, given #{schema.class} instead" + end + + schema.id + end + end + + def discriminator?(data) + data.is_a?(Hash) && data.key?('__type') + end + end + end + + # Contains methods to apply document data to runtime shapes + module Applier + class << self + def apply(schema, data, type = nil) + case schema + when Shapes::StructureShape then apply_structure(schema, data, type) + when Shapes::UnionShape then apply_union(schema, data, type) + when Shapes::ListShape then apply_list(schema, data) + when Shapes::MapShape then apply_map(schema, data) + else data + end + end + + def apply_structure(schema, data, type) + type = schema.type.new if type.nil? + data.each do |k, v| + next if (name = resolve_member_name(schema, k)).nil? + + type[name] = apply(schema.member(name).shape, v) + end + type + end + + def apply_union(schema, data, type) + key, value = data.flatten + return if key.nil? + + if schema.name_by_member_name?(key) + member_name = schema.name_by_member_name(key) + type = schema.member_type(member_name) if type.nil? + type.new(apply(schema.member(member_name).shape, value)) + else + schema.member_type(:unknown).new(key, value) + end + end + + def apply_list(schema, data) + data.map do |v| + next if v.nil? + + apply(schema.member.shape, v) + end + end + + def apply_map(schema, data) + data.transform_values do |v| + if v.nil? + nil + else + apply(schema.value.shape, v) + end + end + end + + private + + def resolve_member_name(schema, key) + return unless schema.name_by_member_name?(key) || schema.member?(key.to_sym) + + schema.name_by_member_name(key) || key.to_sym + end + end + end + end + end +end diff --git a/gems/smithy-schema/spec/smithy-schema/document_spec.rb b/gems/smithy-schema/spec/smithy-schema/document_spec.rb index 5189d5bfe..879e36fdf 100644 --- a/gems/smithy-schema/spec/smithy-schema/document_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/document_spec.rb @@ -5,7 +5,6 @@ module Smithy module Schema describe Document do - # correct handles integer, blob? union? time? let(:runtime_shape) do Struct.new(:string, keyword_init: true) do include Smithy::Schema::Structure @@ -26,6 +25,7 @@ module Schema :list, :foo_map, :structure, + :union, keyword_init: true ) do include Smithy::Schema::Structure @@ -52,7 +52,7 @@ module Schema subject { Document.new('foo') } describe '#initialize' do - it 'sets given data' do + it 'sets data' do expect(subject.data).to eq('foo') end @@ -69,7 +69,7 @@ module Schema end it 'returns nil when member key is not applicable' do - expect(subject['baz']).to be_nil + expect(subject[:bar]).to be_nil end end @@ -99,37 +99,27 @@ module Schema end.to raise_error(ArgumentError) end end - - describe '#as_json' do - # TODO - end end context 'typed document' do - let(:typed_shape) do - aggregate_runtime_shape.new( - string: 'foo', - list: %w[Item1 Item2], - foo_map: { foo: ['Thing'] }, - structure: { list: ['AnotherThing'] } - ) - end + context 'when runtime shape is the input' do + let(:typed_shape) do + aggregate_runtime_shape.new( + string: 'foo', + list: %w[Item1 Item2], + foo_map: { foo: ['Thing1'], bar: ['Thing2'] }, + structure: { list: ['AnotherThing'] } + ) + end - subject { Document.new(typed_shape, aggregate_schema_shape) } + subject { Document.new(typed_shape, aggregate_schema_shape) } - context 'when runtime shape is the input' do describe '#initialize' do - it 'sets data' do - expected_data = { - 'stringMember' => 'foo', - 'listMember' => %w[Item1 Item2], - 'mapMember' => { 'foo' => ['Thing'] }, - 'structureMember' => { 'listMember' => ['AnotherThing'] } - } - expect(subject.data).to eq(expected_data) + it 'set data' do + expect(subject.data).to eq(typed_shape.to_h) end - it 'sets discriminator' do + it 'set discriminator' do expect(subject.discriminator).to be(schema_shape.id) end @@ -147,41 +137,37 @@ module Schema end end - describe '#[]' do - it 'returns member value' do - expect(subject['stringMember']).to eq('foo') - end - - it 'returns nil when member key is not applicable' do - expect(subject['someInvalidMember']).to be_nil - end - end - - describe '#discriminator' do - it 'is not nil' do - expect(subject.discriminator).not_to be_nil - end - end - describe '#as_typed' do it 'converts document as a runtime shape' do typed_shape = subject.as_typed(schema_shape) expect(typed_shape).to be_a(runtime_shape) expect(typed_shape[:string]).to eq('foo') end + end + end - it 'raises when unable to convert as runtime shape' do - # TODO - end + context 'when parsed json is given' do + let(:json) { <<~JSON.strip } + { + "__type": "foo.example#string", + "stringMember": "hello" + } + JSON + + let(:subject) { Document.new(JSON.parse(json)) } + + it 'sets discriminator' do + expect(subject.discriminator).to eq('foo.example#string') end - describe '#as_json' do - # TODO + it 'data does not include a discriminator' do + expect(subject.data).not_to include('__type') end - end - context 'when json is given' do - # TODO + it 'converts document as a runtime shape' do + typed_shape = subject.as_typed(schema_shape) + expect(typed_shape).to be_a(runtime_shape) + end end end end From ff959f1fec6494f92c7fa2ca1cb3ed86296629cf Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 7 Apr 2025 13:24:31 -0700 Subject: [PATCH 08/54] Update file names --- gems/smithy-schema/lib/smithy-schema/document.rb | 4 ++-- .../lib/smithy-schema/{documents.rb => document_utilities.rb} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename gems/smithy-schema/lib/smithy-schema/{documents.rb => document_utilities.rb} (99%) diff --git a/gems/smithy-schema/lib/smithy-schema/document.rb b/gems/smithy-schema/lib/smithy-schema/document.rb index 1392d6915..59bf27275 100644 --- a/gems/smithy-schema/lib/smithy-schema/document.rb +++ b/gems/smithy-schema/lib/smithy-schema/document.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -require_relative 'documents' +require_relative 'document_utilities' module Smithy module Schema # TODO: need to address the following and more # * documentation class Document - include Documents + include DocumentUtilities def initialize(data, schema = nil) @data = format_data(data, schema) @discriminator = Extractor.discriminator(data, schema) diff --git a/gems/smithy-schema/lib/smithy-schema/documents.rb b/gems/smithy-schema/lib/smithy-schema/document_utilities.rb similarity index 99% rename from gems/smithy-schema/lib/smithy-schema/documents.rb rename to gems/smithy-schema/lib/smithy-schema/document_utilities.rb index d6212710b..858da6777 100644 --- a/gems/smithy-schema/lib/smithy-schema/documents.rb +++ b/gems/smithy-schema/lib/smithy-schema/document_utilities.rb @@ -2,7 +2,7 @@ module Smithy module Schema - module Documents + module DocumentUtilities # Contains methods to extract given data as document data module Extractor class << self From 598db66dd5f7d3897f3ba064ddd1dc1faecd737f Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Fri, 11 Apr 2025 13:17:15 -0700 Subject: [PATCH 09/54] More refactoring --- .../lib/smithy-schema/document.rb | 55 +++-- .../lib/smithy-schema/document_utilities.rb | 139 ----------- .../lib/smithy-schema/document_utils.rb | 224 ++++++++++++++++++ .../spec/smithy-schema/document_spec.rb | 178 ++++++++++---- 4 files changed, 390 insertions(+), 206 deletions(-) delete mode 100644 gems/smithy-schema/lib/smithy-schema/document_utilities.rb create mode 100644 gems/smithy-schema/lib/smithy-schema/document_utils.rb diff --git a/gems/smithy-schema/lib/smithy-schema/document.rb b/gems/smithy-schema/lib/smithy-schema/document.rb index 59bf27275..8a9fa77e5 100644 --- a/gems/smithy-schema/lib/smithy-schema/document.rb +++ b/gems/smithy-schema/lib/smithy-schema/document.rb @@ -1,20 +1,18 @@ # frozen_string_literal: true -require_relative 'document_utilities' +require_relative 'document_utils' module Smithy module Schema # TODO: need to address the following and more # * documentation class Document - include DocumentUtilities - def initialize(data, schema = nil) - @data = format_data(data, schema) - @discriminator = Extractor.discriminator(data, schema) - @schema = schema + def initialize(data, options = {}) + @data = set_data(data, options) + @discriminator = extract_discriminator(data, options) end - attr_reader :data, :discriminator, :schema + attr_reader :data, :discriminator def [](key) return unless @data.is_a?(Hash) && @data.key?(key) @@ -22,12 +20,12 @@ def [](key) @data[key] end - def as_typed(schema) + def as_typed(schema, opts = {}) error_message = 'Invalid schema or document data' raise ArgumentError, error_message unless valid_schema?(schema) && @data.is_a?(Hash) type = schema.type.new - Applier.apply(schema, @data, type) + DocumentUtils.apply(@data, schema, type, opts) end private @@ -36,22 +34,49 @@ def discriminator?(data) data.is_a?(Hash) && data.key?('__type') end - def format_data(data, schema) + def extract_discriminator(data, opts) + return if data.nil? + + return unless discriminator?(data) || (schema = opts[:schema]) + + if discriminator?(data) + data['__type'] + else + error_message = "Expected a structure schema, given #{schema.class} instead" + raise error_message unless valid_schema?(schema) + + schema.id + end + end + + def set_data(data, options) return if data.nil? case data when Smithy::Schema::Structure - if schema.nil? || !schema.is_a?(Shapes::StructureShape) - raise ArgumentError, 'Unable to convert as document with given schema' + schema = options[:schema] + if schema.nil? || !valid_schema?(schema) + raise ArgumentError, "Unable to convert to document with given schema: #{schema}" end - Extractor.extract(schema, data) + options = options.except(:schema) + # case 1 - extract data from runtime shape, schema is required to know to properly extract + DocumentUtils.extract(data, schema, options) + else - data = data.except('__type') if discriminator?(data) - data # TODO: add some validation if schema exists + if discriminator?(data) + # case 2 - extract typed data from parsed JSON + # Open question - if there is a schema given, should we validate that these + # two pieces work together? + data.except('__type') + else + # case 3 - untyped data, we will need consolidate timestamps and such + DocumentUtils.format(data) + end end end + # TODO: probably need to check if given runtime shape is a type of schema def valid_schema?(schema) schema.is_a?(Shapes::StructureShape) && !schema.type.nil? end diff --git a/gems/smithy-schema/lib/smithy-schema/document_utilities.rb b/gems/smithy-schema/lib/smithy-schema/document_utilities.rb deleted file mode 100644 index 858da6777..000000000 --- a/gems/smithy-schema/lib/smithy-schema/document_utilities.rb +++ /dev/null @@ -1,139 +0,0 @@ -# frozen_string_literal: true - -module Smithy - module Schema - module DocumentUtilities - # Contains methods to extract given data as document data - module Extractor - class << self - def extract(schema, data) - return nil if data.nil? - - case schema - when Shapes::StructureShape then extract_structure(schema, data) - when Shapes::UnionShape then extract_union(schema, data) - when Shapes::ListShape then extract_list(schema, data) - when Shapes::MapShape then extract_map(schema, data) - else data - end - end - - def extract_structure(schema, data) - data.to_h.each_with_object({}) do |(k, v), o| - next unless schema.member?(k) - - o[k] = extract(schema.member(k).shape, v) - end - end - - def extract_union(schema, data) - output = {} - if data.is_a?(Schema::Union) - member_shape = schema.member_by_type(data.class) - output[member_shape.name] = extract(member_shape.shape, data).value - else - key, value = data.first - if schema.member?(key) - member_shape = schema.member(key) - output[member_shape.name] = extract(member_shape.shape, value) - end - end - output - end - - def extract_list(schema, data) - data.collect { |v| extract(schema.member.shape, v) } - end - - def extract_map(schema, data) - data.each.with_object({}) do |(k, v), h| - h[k] = extract(schema.value.shape, v) - end - end - - def discriminator(data, schema) - return if data.nil? - - if discriminator?(data) - data['__type'] - elsif schema - unless schema.is_a?(Shapes::StructureShape) - raise "Expected a structure schema, given #{schema.class} instead" - end - - schema.id - end - end - - def discriminator?(data) - data.is_a?(Hash) && data.key?('__type') - end - end - end - - # Contains methods to apply document data to runtime shapes - module Applier - class << self - def apply(schema, data, type = nil) - case schema - when Shapes::StructureShape then apply_structure(schema, data, type) - when Shapes::UnionShape then apply_union(schema, data, type) - when Shapes::ListShape then apply_list(schema, data) - when Shapes::MapShape then apply_map(schema, data) - else data - end - end - - def apply_structure(schema, data, type) - type = schema.type.new if type.nil? - data.each do |k, v| - next if (name = resolve_member_name(schema, k)).nil? - - type[name] = apply(schema.member(name).shape, v) - end - type - end - - def apply_union(schema, data, type) - key, value = data.flatten - return if key.nil? - - if schema.name_by_member_name?(key) - member_name = schema.name_by_member_name(key) - type = schema.member_type(member_name) if type.nil? - type.new(apply(schema.member(member_name).shape, value)) - else - schema.member_type(:unknown).new(key, value) - end - end - - def apply_list(schema, data) - data.map do |v| - next if v.nil? - - apply(schema.member.shape, v) - end - end - - def apply_map(schema, data) - data.transform_values do |v| - if v.nil? - nil - else - apply(schema.value.shape, v) - end - end - end - - private - - def resolve_member_name(schema, key) - return unless schema.name_by_member_name?(key) || schema.member?(key.to_sym) - - schema.name_by_member_name(key) || key.to_sym - end - end - end - end - end -end diff --git a/gems/smithy-schema/lib/smithy-schema/document_utils.rb b/gems/smithy-schema/lib/smithy-schema/document_utils.rb new file mode 100644 index 000000000..829c9d072 --- /dev/null +++ b/gems/smithy-schema/lib/smithy-schema/document_utils.rb @@ -0,0 +1,224 @@ +# frozen_string_literal: true + +require 'base64' +require 'time' + +module Smithy + module Schema + # Document Utilities to help (de)construct given data as a document + module DocumentUtils + class << self + + # Used to transform untyped data + def format(data) + return if data.nil? + + case data + when Time + # timestamp format is "epoch-seconds" by default + data.to_i + when Hash + data.transform_values { |v| format(v) } + when Array + data.map { |d| format(d) } + else + data + end + end + + def extract(data, schema, opts = {}) + return if data.nil? + + case resolve_shape(schema) + when Shapes::StructureShape then extract_structure(data, schema, opts) + when Shapes::UnionShape then extract_union(data, schema, opts) + when Shapes::ListShape then extract_list(data, schema) + when Shapes::MapShape then extract_map(data, schema) + when Shapes::BlobShape then extract_blob(data, schema) + when Shapes::TimestampShape then extract_timestamp(data, schema, opts) + else data + end + end + + def apply(data, schema, type = nil, opts = {}) + case resolve_shape(schema) + when Shapes::StructureShape then apply_structure(data, schema, type) + when Shapes::UnionShape then apply_union(data, schema, type) + when Shapes::ListShape then apply_list(data, schema) + when Shapes::MapShape then apply_map(data, schema) + when Shapes::TimestampShape then apply_timestamp(data, schema, opts) + when Shapes::BlobShape then Base64.decode64(data) + else data + end + end + + private + + def apply_structure(data, schema, type) + shape = resolve_shape(schema) + + type = shape.type.new if type.nil? + data.each do |k, v| + name = + if (member = json_name_member(k, shape)) + shape.name_by_member_name(member.name) + else + member_name(shape, k) + end + next if name.nil? + + type[name] = apply(v, shape.member(name)) + end + type + end + + def apply_timestamp(data, schema, opts) + data = data.is_a?(Numeric) ? Time.at(data) : Time.parse(data) + trait = resolve_timestamp_trait(schema) if opts[:use_timestamp_format] + time(data, trait) + end + + def apply_union(data, schema, type) + shape = resolve_shape(schema) + key, value = data.flatten + return if key.nil? + + if (member = json_name_member(key, shape)) + member_name = shape.name_by_member_name(member.name) + type = shape.member_type(member_name) if type.nil? + type.new(apply(value, shape.member(member_name))) + elsif shape.name_by_member_name?(key) + member_name = shape.name_by_member_name(key) + type = shape.member_type(member_name) if type.nil? + type.new(apply(value, shape.member(member_name))) + else + shape.member_type(:unknown).new(key, value) + end + end + + def json_name_member(name, shape) + shape.members.values.find do |v| + v.traits['smithy.api#jsonName'] == name if v.traits.include?('smithy.api#jsonName') + end + end + + + def apply_list(data, schema) + shape = resolve_shape(schema) + data.map do |v| + next if v.nil? + + apply(v, shape.member) + end + end + + def apply_map(data, schema) + shape = resolve_shape(schema) + data.transform_values do |v| + if v.nil? + nil + else + apply(v, shape.value) + end + end + end + + def extract_structure(data, schema, opts) + shape = resolve_shape(schema) + data.to_h.each_with_object({}) do |(k, v), o| + next unless shape.member?(k) + + member_shape = shape.member(k) + member_name = resolve_member_name(member_shape, opts) + o[member_name] = extract(v, member_shape, opts) + end + end + + def extract_union(data, schema, opts) + h = {} + shape = resolve_shape(schema) + if data.is_a?(Schema::Union) + member_shape = shape.member_by_type(data.class) + member_name = resolve_member_name(member_shape, opts) + h[member_name] = extract(data, member_shape).value + else + key, value = data.first + if shape.member?(key) + member_shape = shape.member(key) + member_name = resolve_member_name(member_shape, opts) + h[member_name] = extract(value, member_shape) + end + end + h + end + + def extract_list(data, schema) + shape = resolve_shape(schema) + data.collect { |v| extract(v, shape.member) } + end + + def extract_map(data, schema) + shape = resolve_shape(schema) + data.each.with_object({}) do |(k, v), h| + h[k] = extract(v, shape.value) + end + end + + def extract_blob(data, _schema) + Base64.strict_encode64(data.is_a?(String) ? data : data.read) + end + + def extract_timestamp(data, schema, opts) + return unless data.is_a?(Time) + + trait = resolve_timestamp_trait(schema) if opts[:use_timestamp_format] + time(data, trait) + end + + def resolve_shape(schema) + schema.is_a?(Shapes::MemberShape) ? schema.shape : schema + end + + def resolve_member_name(member_shape, opts) + if opts[:use_json_name] && member_shape.traits['smithy.api#jsonName'] + member_shape.traits['smithy.api#jsonName'] + else + member_shape.name + end + end + + def member_name(schema, key) + return unless schema.name_by_member_name?(key) || schema.member?(key.to_sym) + + schema.name_by_member_name(key) || key.to_sym + end + + def resolve_timestamp_trait(schema) + if schema.is_a?(Shapes::MemberShape) + schema.traits['smithy.api#timestampFormat'] + else + schema.shape.traits['smithy.api#timestampFormat'] + end + end + + def time(data, trait = nil) + if trait + case trait + when 'http-date' + data.utc.iso8601 + when 'date-time' + data.utc.httpdate + when 'epoch-seconds' + data.utc.to_i + else + raise "unhandled timestamp format `#{value}`" + end + else + # timestamp format is "epoch-seconds" by default + data.utc.to_i + end + end + end + end + end +end diff --git a/gems/smithy-schema/spec/smithy-schema/document_spec.rb b/gems/smithy-schema/spec/smithy-schema/document_spec.rb index 879e36fdf..aa69f5c5a 100644 --- a/gems/smithy-schema/spec/smithy-schema/document_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/document_spec.rb @@ -5,46 +5,71 @@ module Smithy module Schema describe Document do - let(:runtime_shape) do + + let(:simple_runtime) do Struct.new(:string, keyword_init: true) do include Smithy::Schema::Structure end end - let(:schema_shape) do - string_shape = Shapes::StringShape.new(id: 'smithy.api#String') - shape = Shapes::StructureShape.new(id: 'smithy.ruby.tests#Structure') - shape.add_member(:string, 'stringMember', string_shape) - shape.type = runtime_shape + let(:simple_schema) do + shape = Shapes::StructureShape.new(id: 'smithy.ruby.tests#SimpleStructure') + string = Shapes::StringShape.new(id: 'smithy.api#String') + shape.add_member(:string, 'stringMember', string) + shape.type = simple_runtime shape end - let(:aggregate_runtime_shape) do - Struct.new( - :string, - :list, - :foo_map, - :structure, - :union, - keyword_init: true - ) do + let(:runtime) do + Struct.new(:string, :list, :foo_map, :structure, :union, :blob, :timestamp, keyword_init: true) do include Smithy::Schema::Structure end end - let(:aggregate_schema_shape) do - string_shape = Shapes::StringShape.new(id: 'smithy.api#String') - list_shape = Shapes::ListShape.new(id: 'smithy.ruby.tests#List') - list_shape.set_member(Shapes::Prelude::String) - map_shape = Shapes::MapShape.new(id: 'smithy.ruby.tests#Map') - map_shape.set_key(Shapes::Prelude::String) - map_shape.set_value(list_shape) + let(:union_runtime) { Class.new(Union) } + let(:union_value_runtime) do + Class.new(union_runtime) do + def to_h + { union_string: super(__getobj__) } + end + + # anonymous class, need a class name to test to_s + def self.name + 'TestUnion::UnionString' + end + end + end + + let(:schema) do shape = Shapes::StructureShape.new(id: 'smithy.ruby.tests#Structure') - shape.add_member(:string, 'stringMember', string_shape) - shape.add_member(:list, 'listMember', list_shape) - shape.add_member(:foo_map, 'mapMember', map_shape) + string = Shapes::StringShape.new(id: 'smithy.api#String') + list = Shapes::ListShape.new(id: 'smithy.ruby.tests#List') + list.set_member(Shapes::Prelude::String) + map = Shapes::MapShape.new(id: 'smithy.ruby.tests#Map') + map.set_key(Shapes::Prelude::String) + map.set_value(list) + union = Shapes::UnionShape.new(id: 'smithy.ruby.tests#Union') + union.add_member( + :union_string, + 'unionString', + string, + union_value_runtime, + traits: { 'smithy.api#jsonName' => 'json' } + ) + union.type = union_runtime + shape.add_member(:string, 'stringMember', string, traits: { 'smithy.api#jsonName' => 'json' }) + shape.add_member(:list, 'listMember', list) + shape.add_member(:foo_map, 'mapMember', map) + shape.add_member(:union, 'unionMember', union) + shape.add_member( + :timestamp, + 'timeMember', + Shapes::TimestampShape.new(id: 'smithy.ruby.tests#Timestamp'), + traits: { 'smithy.api#timestampFormat' => 'http-date' } + ) + shape.add_member(:blob, 'blobMember', Shapes::BlobShape.new(id: 'smithy.ruby.tests#Blob')) shape.add_member(:structure, 'structureMember', shape) - shape.type = aggregate_runtime_shape + shape.type = runtime shape end @@ -56,6 +81,11 @@ module Schema expect(subject.data).to eq('foo') end + it 'sets time data using default format' do + doc = Document.new(Time.utc(2024, 12, 25)) + expect(doc.data).to eq(1_735_084_800) + end + it 'defaults discriminator to nil' do expect(subject.discriminator).to be_nil end @@ -81,46 +111,64 @@ module Schema describe '#as_typed' do it 'converts document as runtime shape' do - typed_shape = Document.new({ string: 'foo' }).as_typed(schema_shape) - expect(typed_shape).to be_a(runtime_shape) + typed_shape = Document.new({ string: 'foo' }).as_typed(simple_schema) + expect(typed_shape).to be_a(simple_runtime) expect(typed_shape[:string]).to eq('foo') end it 'raises when invalid schema is given' do - invalid_schema = Shapes::StringShape.new(id: 'smithy.api#String') + invalid_schema = Shapes::StringShape.new(id: 'smithy.api#Invalid') expect do subject.as_typed(invalid_schema) end.to raise_error(ArgumentError) end - - it 'raises when document cannot be converted' do - expect do - subject.as_typed(schema_shape) - end.to raise_error(ArgumentError) - end end end context 'typed document' do context 'when runtime shape is the input' do let(:typed_shape) do - aggregate_runtime_shape.new( + runtime.new( string: 'foo', list: %w[Item1 Item2], foo_map: { foo: ['Thing1'], bar: ['Thing2'] }, - structure: { list: ['AnotherThing'] } + structure: { list: ['AnotherThing'] }, + union: { union_string: 'hello world' }, + timestamp: Time.utc(2024, 12, 25), + blob: StringIO.new('foo') ) end - subject { Document.new(typed_shape, aggregate_schema_shape) } + subject { Document.new(typed_shape, schema: schema) } describe '#initialize' do it 'set data' do - expect(subject.data).to eq(typed_shape.to_h) + expect(subject.data).to include( + { + 'stringMember' => 'foo', + 'listMember' => %w[Item1 Item2], + 'mapMember' => { foo: ['Thing1'], bar: ['Thing2'] }, + 'structureMember' => { 'listMember' => ['AnotherThing'] }, + 'unionMember' => { 'unionString' => 'hello world' }, + 'timeMember' => 1_735_084_800, + 'blobMember' => 'Zm9v' + } + ) + end + + it 'set data using jsonName when applicable' do + typed_shape = runtime.new(string: 'foo', union: { union_string: 'bar' }) + doc = Document.new(typed_shape, schema: schema, use_json_name: true) + expect(doc.data).to include({ 'json' => 'foo', 'unionMember' => { 'json' => 'bar' } }) + end + + it 'set data using timestampTrait when applicable' do + doc = Document.new(typed_shape, schema: schema, use_timestamp_format: true) + expect(doc.data['timeMember']).to eq('2024-12-25T00:00:00Z') end it 'set discriminator' do - expect(subject.discriminator).to be(schema_shape.id) + expect(subject.discriminator).to be(schema.id) end it 'raises when no schema is given' do @@ -129,7 +177,7 @@ module Schema end.to raise_error(ArgumentError) end - it 'raises when unable to deconstruct data with schema' do + it 'raises when an invalid schema is provided' do invalid_schema = Shapes::StringShape.new(id: 'smithy.api#String') expect do Document.new(typed_shape, invalid_schema) @@ -139,14 +187,36 @@ module Schema describe '#as_typed' do it 'converts document as a runtime shape' do - typed_shape = subject.as_typed(schema_shape) - expect(typed_shape).to be_a(runtime_shape) + typed_shape = subject.as_typed(schema) + expect(typed_shape.to_h).to include( + { + string: 'foo', + list: %w[Item1 Item2], + foo_map: { foo: ['Thing1'], bar: ['Thing2'] }, + structure: { list: ['AnotherThing'] }, + union: { union_string: 'hello world' }, + timestamp: 1_735_084_800, + blob: 'foo' + } + ) + end + + it 'converts document as a runtime shape of a similar schema' do + typed_shape = subject.as_typed(simple_schema) + expect(typed_shape).to be_a(simple_runtime) expect(typed_shape[:string]).to eq('foo') end + + it 'converts document with jsonName trait as a runtime shape' do + typed_shape = runtime.new(string: 'foo', union: { union_string: 'bar' }) + doc = Document.new(typed_shape, schema: schema, use_json_name: true).as_typed(schema) + expect(doc.string).to eq('foo') + expect(doc.union.value).to eq('bar') + end end end - context 'when parsed json is given' do + context 'when parsed json is the input' do let(:json) { <<~JSON.strip } { "__type": "foo.example#string", @@ -156,17 +226,21 @@ module Schema let(:subject) { Document.new(JSON.parse(json)) } - it 'sets discriminator' do - expect(subject.discriminator).to eq('foo.example#string') - end + describe '#initialize' do + it 'sets discriminator' do + expect(subject.discriminator).to eq('foo.example#string') + end - it 'data does not include a discriminator' do - expect(subject.data).not_to include('__type') + it 'data does not include a discriminator' do + expect(subject.data).not_to include('__type') + end end - it 'converts document as a runtime shape' do - typed_shape = subject.as_typed(schema_shape) - expect(typed_shape).to be_a(runtime_shape) + describe '#as_typed' do + it 'converts document as a runtime shape' do + typed_shape = subject.as_typed(simple_schema) + expect(typed_shape).to be_a(simple_runtime) + end end end end From 269b2b56923cbe7f382e0a7af000f738a107f5f8 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Fri, 11 Apr 2025 13:37:19 -0700 Subject: [PATCH 10/54] Remove scratches --- gems/smithy-schema/lib/smithy-schema/document.rb | 3 --- gems/smithy-schema/spec/spec_helper.rb | 1 - 2 files changed, 4 deletions(-) diff --git a/gems/smithy-schema/lib/smithy-schema/document.rb b/gems/smithy-schema/lib/smithy-schema/document.rb index 8a9fa77e5..3ef52ac69 100644 --- a/gems/smithy-schema/lib/smithy-schema/document.rb +++ b/gems/smithy-schema/lib/smithy-schema/document.rb @@ -66,8 +66,6 @@ def set_data(data, options) else if discriminator?(data) # case 2 - extract typed data from parsed JSON - # Open question - if there is a schema given, should we validate that these - # two pieces work together? data.except('__type') else # case 3 - untyped data, we will need consolidate timestamps and such @@ -76,7 +74,6 @@ def set_data(data, options) end end - # TODO: probably need to check if given runtime shape is a type of schema def valid_schema?(schema) schema.is_a?(Shapes::StructureShape) && !schema.type.nil? end diff --git a/gems/smithy-schema/spec/spec_helper.rb b/gems/smithy-schema/spec/spec_helper.rb index 2d654eb6a..b6e8d700b 100644 --- a/gems/smithy-schema/spec/spec_helper.rb +++ b/gems/smithy-schema/spec/spec_helper.rb @@ -4,7 +4,6 @@ SimpleCov.start require 'smithy-schema' -require_relative 'support/schema_helper' # This file was generated by the `rspec --init` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. From 90c58ce1a28f701a7bc8db9ce35b59243190eedf Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Fri, 11 Apr 2025 13:46:39 -0700 Subject: [PATCH 11/54] Fix rubocop --- .../lib/smithy-schema/document_utils.rb | 44 ++++++++++--------- .../spec/smithy-schema/document_spec.rb | 1 - 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/gems/smithy-schema/lib/smithy-schema/document_utils.rb b/gems/smithy-schema/lib/smithy-schema/document_utils.rb index 829c9d072..1a908773c 100644 --- a/gems/smithy-schema/lib/smithy-schema/document_utils.rb +++ b/gems/smithy-schema/lib/smithy-schema/document_utils.rb @@ -8,7 +8,6 @@ module Schema # Document Utilities to help (de)construct given data as a document module DocumentUtils class << self - # Used to transform untyped data def format(data) return if data.nil? @@ -26,6 +25,19 @@ def format(data) end end + def apply(data, schema, type = nil, opts = {}) + case resolve_shape(schema) + when Shapes::StructureShape then apply_structure(data, schema, type) + when Shapes::UnionShape then apply_union(data, schema, type) + when Shapes::ListShape then apply_list(data, schema) + when Shapes::MapShape then apply_map(data, schema) + when Shapes::TimestampShape then apply_timestamp(data, schema, opts) + when Shapes::BlobShape then Base64.decode64(data) + else data + end + end + + # rubocop:disable Metrics/CyclomaticComplexity def extract(data, schema, opts = {}) return if data.nil? @@ -39,18 +51,7 @@ def extract(data, schema, opts = {}) else data end end - - def apply(data, schema, type = nil, opts = {}) - case resolve_shape(schema) - when Shapes::StructureShape then apply_structure(data, schema, type) - when Shapes::UnionShape then apply_union(data, schema, type) - when Shapes::ListShape then apply_list(data, schema) - when Shapes::MapShape then apply_map(data, schema) - when Shapes::TimestampShape then apply_timestamp(data, schema, opts) - when Shapes::BlobShape then Base64.decode64(data) - else data - end - end + # rubocop:enable Metrics/CyclomaticComplexity private @@ -78,6 +79,7 @@ def apply_timestamp(data, schema, opts) time(data, trait) end + # rubocop:disable Metrics/AbcSize def apply_union(data, schema, type) shape = resolve_shape(schema) key, value = data.flatten @@ -95,6 +97,7 @@ def apply_union(data, schema, type) shape.member_type(:unknown).new(key, value) end end + # rubocop:enable Metrics/AbcSize def json_name_member(name, shape) shape.members.values.find do |v| @@ -102,7 +105,6 @@ def json_name_member(name, shape) end end - def apply_list(data, schema) shape = resolve_shape(schema) data.map do |v| @@ -134,6 +136,7 @@ def extract_structure(data, schema, opts) end end + # rubocop:disable Metrics/AbcSize def extract_union(data, schema, opts) h = {} shape = resolve_shape(schema) @@ -151,6 +154,7 @@ def extract_union(data, schema, opts) end h end + # rubocop:enable Metrics/AbcSize def extract_list(data, schema) shape = resolve_shape(schema) @@ -175,6 +179,12 @@ def extract_timestamp(data, schema, opts) time(data, trait) end + def member_name(schema, key) + return unless schema.name_by_member_name?(key) || schema.member?(key.to_sym) + + schema.name_by_member_name(key) || key.to_sym + end + def resolve_shape(schema) schema.is_a?(Shapes::MemberShape) ? schema.shape : schema end @@ -187,12 +197,6 @@ def resolve_member_name(member_shape, opts) end end - def member_name(schema, key) - return unless schema.name_by_member_name?(key) || schema.member?(key.to_sym) - - schema.name_by_member_name(key) || key.to_sym - end - def resolve_timestamp_trait(schema) if schema.is_a?(Shapes::MemberShape) schema.traits['smithy.api#timestampFormat'] diff --git a/gems/smithy-schema/spec/smithy-schema/document_spec.rb b/gems/smithy-schema/spec/smithy-schema/document_spec.rb index aa69f5c5a..9da543814 100644 --- a/gems/smithy-schema/spec/smithy-schema/document_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/document_spec.rb @@ -5,7 +5,6 @@ module Smithy module Schema describe Document do - let(:simple_runtime) do Struct.new(:string, keyword_init: true) do include Smithy::Schema::Structure From a1e46cc5ee092bd12e90b119427bbf95523eca35 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 14 Apr 2025 08:38:51 -0700 Subject: [PATCH 12/54] Clean up document --- .../lib/smithy-schema/document.rb | 36 +++-- .../lib/smithy-schema/document_utils.rb | 151 +++++++++--------- .../lib/smithy-schema/structure.rb | 2 +- 3 files changed, 104 insertions(+), 85 deletions(-) diff --git a/gems/smithy-schema/lib/smithy-schema/document.rb b/gems/smithy-schema/lib/smithy-schema/document.rb index 3ef52ac69..fe17a3517 100644 --- a/gems/smithy-schema/lib/smithy-schema/document.rb +++ b/gems/smithy-schema/lib/smithy-schema/document.rb @@ -4,28 +4,44 @@ module Smithy module Schema - # TODO: need to address the following and more - # * documentation + # A Smithy document type, representing typed or untyped data from Smithy data model. class Document + # @param [Object] data document data + # @param [Hash] options + # @option options [Smithy::Schema::Structure] :schema schema to reference when setting + # document data. Only applicable when data param is a type of {Shapes::StructureShape}. + # @option options [Boolean] :use_timestamp_format Whether to use the `timestampFormat` + # trait or ignore it when creating a {Document} with given schema. The `timestampFormat` + # trait is ignored by default. + # @option options [Boolean] :use_json_name Whether to use the `jsonName` trait or ignore + # it when creating a {Document} with given schema. The `jsonName` trait is ignored + # by default. def initialize(data, options = {}) @data = set_data(data, options) @discriminator = extract_discriminator(data, options) end - attr_reader :data, :discriminator + # @return [Object] data + attr_reader :data + # @return [String] discriminator + attr_reader :discriminator + + # @param [Object] key + # @return [Object] def [](key) return unless @data.is_a?(Hash) && @data.key?(key) @data[key] end - def as_typed(schema, opts = {}) + # @param [Smithy::Schema::Shapes::StructureShape] schema + def as_typed(schema) error_message = 'Invalid schema or document data' raise ArgumentError, error_message unless valid_schema?(schema) && @data.is_a?(Hash) type = schema.type.new - DocumentUtils.apply(@data, schema, type, opts) + DocumentUtils.apply(@data, schema, type) end private @@ -49,19 +65,19 @@ def extract_discriminator(data, opts) end end - def set_data(data, options) + def set_data(data, opts) return if data.nil? case data when Smithy::Schema::Structure - schema = options[:schema] + schema = opts[:schema] if schema.nil? || !valid_schema?(schema) - raise ArgumentError, "Unable to convert to document with given schema: #{schema}" + raise ArgumentError, "Unable to create a document with given schema: #{schema}" end - options = options.except(:schema) + opts = opts.except(:schema) # case 1 - extract data from runtime shape, schema is required to know to properly extract - DocumentUtils.extract(data, schema, options) + DocumentUtils.extract(data, schema, opts) else if discriminator?(data) diff --git a/gems/smithy-schema/lib/smithy-schema/document_utils.rb b/gems/smithy-schema/lib/smithy-schema/document_utils.rb index 1a908773c..610e24c0b 100644 --- a/gems/smithy-schema/lib/smithy-schema/document_utils.rb +++ b/gems/smithy-schema/lib/smithy-schema/document_utils.rb @@ -5,7 +5,8 @@ module Smithy module Schema - # Document Utilities to help (de)construct given data as a document + # @api private + # Document Utilities to help (de)construct data to/from Smithy document module DocumentUtils class << self # Used to transform untyped data @@ -14,8 +15,7 @@ def format(data) case data when Time - # timestamp format is "epoch-seconds" by default - data.to_i + data.to_i # timestamp format is "epoch-seconds" by default when Hash data.transform_values { |v| format(v) } when Array @@ -25,13 +25,14 @@ def format(data) end end - def apply(data, schema, type = nil, opts = {}) - case resolve_shape(schema) + # Used to apply data to runtime shape + def apply(data, schema, type = nil) + case shape(schema) when Shapes::StructureShape then apply_structure(data, schema, type) when Shapes::UnionShape then apply_union(data, schema, type) when Shapes::ListShape then apply_list(data, schema) when Shapes::MapShape then apply_map(data, schema) - when Shapes::TimestampShape then apply_timestamp(data, schema, opts) + when Shapes::TimestampShape then apply_timestamp(data, schema) when Shapes::BlobShape then Base64.decode64(data) else data end @@ -41,12 +42,12 @@ def apply(data, schema, type = nil, opts = {}) def extract(data, schema, opts = {}) return if data.nil? - case resolve_shape(schema) + case shape(schema) when Shapes::StructureShape then extract_structure(data, schema, opts) when Shapes::UnionShape then extract_union(data, schema, opts) when Shapes::ListShape then extract_list(data, schema) when Shapes::MapShape then extract_map(data, schema) - when Shapes::BlobShape then extract_blob(data, schema) + when Shapes::BlobShape then extract_blob(data) when Shapes::TimestampShape then extract_timestamp(data, schema, opts) else data end @@ -55,13 +56,33 @@ def extract(data, schema, opts = {}) private + def apply_list(data, schema) + shape = shape(schema) + data.map do |v| + next if v.nil? + + apply(v, shape.member) + end + end + + def apply_map(data, schema) + shape = shape(schema) + data.transform_values do |v| + if v.nil? + nil + else + apply(v, shape.value) + end + end + end + def apply_structure(data, schema, type) - shape = resolve_shape(schema) + shape = shape(schema) type = shape.type.new if type.nil? data.each do |k, v| name = - if (member = json_name_member(k, shape)) + if (member = member_with_json_name(k, shape)) shape.name_by_member_name(member.name) else member_name(shape, k) @@ -73,60 +94,47 @@ def apply_structure(data, schema, type) type end - def apply_timestamp(data, schema, opts) + def apply_timestamp(data, schema) data = data.is_a?(Numeric) ? Time.at(data) : Time.parse(data) - trait = resolve_timestamp_trait(schema) if opts[:use_timestamp_format] - time(data, trait) + time(data, timestamp_format(schema)) end - # rubocop:disable Metrics/AbcSize def apply_union(data, schema, type) - shape = resolve_shape(schema) + shape = shape(schema) key, value = data.flatten return if key.nil? - if (member = json_name_member(key, shape)) - member_name = shape.name_by_member_name(member.name) - type = shape.member_type(member_name) if type.nil? - type.new(apply(value, shape.member(member_name))) + if (member = member_with_json_name(key, shape)) + apply_union_member(member.name, shape, type) elsif shape.name_by_member_name?(key) - member_name = shape.name_by_member_name(key) - type = shape.member_type(member_name) if type.nil? - type.new(apply(value, shape.member(member_name))) + apply_union_member(key, shape, type) else shape.member_type(:unknown).new(key, value) end end - # rubocop:enable Metrics/AbcSize - def json_name_member(name, shape) - shape.members.values.find do |v| - v.traits['smithy.api#jsonName'] == name if v.traits.include?('smithy.api#jsonName') - end + def apply_union_member(key, shape, type) + member_name = shape.name_by_member_name(key) + type = shape.member_type(member_name) if type.nil? + type.new(apply(value, shape.member(member_name))) end - def apply_list(data, schema) - shape = resolve_shape(schema) - data.map do |v| - next if v.nil? + def extract_blob(data) + Base64.strict_encode64(data.is_a?(String) ? data : data.read) + end - apply(v, shape.member) - end + def extract_list(data, schema) + shape = shape(schema) + data.collect { |v| extract(v, shape.member) } end - def apply_map(data, schema) - shape = resolve_shape(schema) - data.transform_values do |v| - if v.nil? - nil - else - apply(v, shape.value) - end - end + def extract_map(data, schema) + shape = shape(schema) + data.each.with_object({}) { |(k, v), h| h[k] = extract(v, shape.value) } end def extract_structure(data, schema, opts) - shape = resolve_shape(schema) + shape = shape(schema) data.to_h.each_with_object({}) do |(k, v), o| next unless shape.member?(k) @@ -136,10 +144,17 @@ def extract_structure(data, schema, opts) end end + def extract_timestamp(data, schema, opts) + return unless data.is_a?(Time) + + trait = timestamp_format(schema) if opts[:use_timestamp_format] + time(data, trait) + end + # rubocop:disable Metrics/AbcSize def extract_union(data, schema, opts) h = {} - shape = resolve_shape(schema) + shape = shape(schema) if data.is_a?(Schema::Union) member_shape = shape.member_by_type(data.class) member_name = resolve_member_name(member_shape, opts) @@ -156,37 +171,16 @@ def extract_union(data, schema, opts) end # rubocop:enable Metrics/AbcSize - def extract_list(data, schema) - shape = resolve_shape(schema) - data.collect { |v| extract(v, shape.member) } - end - - def extract_map(data, schema) - shape = resolve_shape(schema) - data.each.with_object({}) do |(k, v), h| - h[k] = extract(v, shape.value) - end - end - - def extract_blob(data, _schema) - Base64.strict_encode64(data.is_a?(String) ? data : data.read) - end - - def extract_timestamp(data, schema, opts) - return unless data.is_a?(Time) - - trait = resolve_timestamp_trait(schema) if opts[:use_timestamp_format] - time(data, trait) - end - def member_name(schema, key) return unless schema.name_by_member_name?(key) || schema.member?(key.to_sym) schema.name_by_member_name(key) || key.to_sym end - def resolve_shape(schema) - schema.is_a?(Shapes::MemberShape) ? schema.shape : schema + def member_with_json_name(name, shape) + shape.members.values.find do |v| + v.traits['smithy.api#jsonName'] == name if v.traits.include?('smithy.api#jsonName') + end end def resolve_member_name(member_shape, opts) @@ -197,11 +191,21 @@ def resolve_member_name(member_shape, opts) end end - def resolve_timestamp_trait(schema) - if schema.is_a?(Shapes::MemberShape) + def shape(schema) + schema.is_a?(Shapes::MemberShape) ? schema.shape : schema + end + + # The following steps are taken to determine the format of timestamp: + # Use the timestampFormat trait of the member, if present. + # Use the timestampFormat trait of the shape, if present. + # If none of the above applies, use epoch-seconds as default + def timestamp_format(schema) + if schema.traits['smithy.api#timestampFormat'] schema.traits['smithy.api#timestampFormat'] - else + elsif schema.shape.traits['smithy.api#timestampFormat'] schema.shape.traits['smithy.api#timestampFormat'] + else + 'epoch-seconds' end end @@ -218,8 +222,7 @@ def time(data, trait = nil) raise "unhandled timestamp format `#{value}`" end else - # timestamp format is "epoch-seconds" by default - data.utc.to_i + data.utc.to_i # default format end end end diff --git a/gems/smithy-schema/lib/smithy-schema/structure.rb b/gems/smithy-schema/lib/smithy-schema/structure.rb index 8cb5fee97..df183b8f5 100644 --- a/gems/smithy-schema/lib/smithy-schema/structure.rb +++ b/gems/smithy-schema/lib/smithy-schema/structure.rb @@ -43,7 +43,7 @@ def _to_h_array(obj) end end - # An empty Struct that includes the {Client::Structure} module. + # An empty Struct that includes the {Schema::Structure} module. EmptyStructure = Struct.new do include Smithy::Schema::Structure end From 8b666cdd4b71cd88fa3db489d6dc6c84c1e488ff Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 14 Apr 2025 08:50:45 -0700 Subject: [PATCH 13/54] Clean document specs --- gems/smithy-schema/lib/smithy-schema/document_utils.rb | 6 +++--- gems/smithy-schema/spec/smithy-schema/document_spec.rb | 9 +-------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/gems/smithy-schema/lib/smithy-schema/document_utils.rb b/gems/smithy-schema/lib/smithy-schema/document_utils.rb index 610e24c0b..726d0b47f 100644 --- a/gems/smithy-schema/lib/smithy-schema/document_utils.rb +++ b/gems/smithy-schema/lib/smithy-schema/document_utils.rb @@ -105,15 +105,15 @@ def apply_union(data, schema, type) return if key.nil? if (member = member_with_json_name(key, shape)) - apply_union_member(member.name, shape, type) + apply_union_member(member.name, value, shape, type) elsif shape.name_by_member_name?(key) - apply_union_member(key, shape, type) + apply_union_member(key, value, shape, type) else shape.member_type(:unknown).new(key, value) end end - def apply_union_member(key, shape, type) + def apply_union_member(key, value, shape, type) member_name = shape.name_by_member_name(key) type = shape.member_type(member_name) if type.nil? type.new(apply(value, shape.member(member_name))) diff --git a/gems/smithy-schema/spec/smithy-schema/document_spec.rb b/gems/smithy-schema/spec/smithy-schema/document_spec.rb index 9da543814..9064631e2 100644 --- a/gems/smithy-schema/spec/smithy-schema/document_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/document_spec.rb @@ -194,7 +194,7 @@ def self.name foo_map: { foo: ['Thing1'], bar: ['Thing2'] }, structure: { list: ['AnotherThing'] }, union: { union_string: 'hello world' }, - timestamp: 1_735_084_800, + timestamp: '2024-12-25T00:00:00Z', blob: 'foo' } ) @@ -205,13 +205,6 @@ def self.name expect(typed_shape).to be_a(simple_runtime) expect(typed_shape[:string]).to eq('foo') end - - it 'converts document with jsonName trait as a runtime shape' do - typed_shape = runtime.new(string: 'foo', union: { union_string: 'bar' }) - doc = Document.new(typed_shape, schema: schema, use_json_name: true).as_typed(schema) - expect(doc.string).to eq('foo') - expect(doc.union.value).to eq('bar') - end end end From 2ddf4bdfa76039ca3f61cca2814489b90b192d8b Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 14 Apr 2025 12:08:54 -0700 Subject: [PATCH 14/54] Update TypeRegistry --- .../lib/smithy-schema/document.rb | 3 +- .../lib/smithy-schema/type_registry.rb | 67 ++++++++++++++----- 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/gems/smithy-schema/lib/smithy-schema/document.rb b/gems/smithy-schema/lib/smithy-schema/document.rb index fe17a3517..88fa4cdc1 100644 --- a/gems/smithy-schema/lib/smithy-schema/document.rb +++ b/gems/smithy-schema/lib/smithy-schema/document.rb @@ -35,7 +35,8 @@ def [](key) @data[key] end - # @param [Smithy::Schema::Shapes::StructureShape] schema + # @param [Shapes::Shape] schema + # @return [Shapes::Structure] typed shape def as_typed(schema) error_message = 'Invalid schema or document data' raise ArgumentError, error_message unless valid_schema?(schema) && @data.is_a?(Hash) diff --git a/gems/smithy-schema/lib/smithy-schema/type_registry.rb b/gems/smithy-schema/lib/smithy-schema/type_registry.rb index a15aaa1f9..ffefa9d88 100644 --- a/gems/smithy-schema/lib/smithy-schema/type_registry.rb +++ b/gems/smithy-schema/lib/smithy-schema/type_registry.rb @@ -2,14 +2,17 @@ module Smithy module Schema - # Registry that contains a map of Smithy shape ID to its shape representation - # TODO: Implement a method that takes a document and deserializes - # Do we include operation shapes in this registry? + # A registry that contains a map of Smithy shape ID to its schema. + # Also includes a way to find schema based on its shape type representation class TypeRegistry def initialize @registry = {} + @schema_by_types = {} end + # @api private + attr_reader :schema_by_types + # @return [Hash] attr_accessor :registry @@ -18,28 +21,62 @@ def register(*shapes) raise ArgumentError, 'Expected an array of Shapes' unless shapes.all?(Shapes::Shape) shapes.each do |s| - next if s.id.nil? - @registry[s.id] = s + + case s.class + when Shapes::StructureShape + @schema_by_types[s.type] = s if s.type + when Shapes::UnionShape + s.member_types.values { |v| @schema_by_types[v] = s } + end end end - # @param [String] shape_id + # Returns true if this type registry contains specific shape id. + # @param [String] id + # @return [Boolean] + def schema_by_id?(id) + @registry.key?(id) + end + + # Returns the shape schema registered for the given shape id. + # @param [id] id + # @return [Shapes::Shape, nil] + def schema_by_id(id) + @registry[id] + end + + # Returns true if this type registry contains a schema associated + # with the given typed shape. + # @param [Class] type # @return [Boolean] - def shape?(shape_id) - @registry.key?(shape_id) + def scheme_by_type?(type) + @schema_by_types.key?(type) + end + + # Returns the shape schema registered for the given typed shape. + # @param [Class] type + # @return [Shapes::Shape, nil] + def scheme_by_type(type) + @schema_by_types[type] end - # @param [String] shape_id - # @return [Shapes::Shape] - def shape(shape_id) - @registry[shape_id] + # Deserializes a document into a typed shape from registry. + # @param [Document] document + # @return [Shapes::Structure] typed shape + def convert_as_typed(document) + msg = 'Unable to convert given document since discriminator is not set' + raise ArgumentError, msg unless document.discriminator + + if schema_by_id?(document.discriminator) + document.as_typed(schema_by_id(document.discriminator)) + else + msg = "Unable to find schema with #{document.discriminator} in Registry" + raise ArgumentError, msg + end end class << self - # TODO: Need thoughts on... - # * Smithy-Java only allows to compose 2 type registries at a time. - # Do we follow suit or allow unlimited number of registry to compose? # @param [Array] # @return [TypeRegistry] def compose(*type_registries) From 112ddf4eb8c830362063df2eb2572d17b43e35c2 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 14 Apr 2025 12:24:59 -0700 Subject: [PATCH 15/54] Add documentation --- gems/smithy-schema/lib/smithy-schema/document.rb | 5 +++++ gems/smithy-schema/lib/smithy-schema/type_registry.rb | 1 + 2 files changed, 6 insertions(+) diff --git a/gems/smithy-schema/lib/smithy-schema/document.rb b/gems/smithy-schema/lib/smithy-schema/document.rb index 88fa4cdc1..de4720994 100644 --- a/gems/smithy-schema/lib/smithy-schema/document.rb +++ b/gems/smithy-schema/lib/smithy-schema/document.rb @@ -5,6 +5,11 @@ module Smithy module Schema # A Smithy document type, representing typed or untyped data from Smithy data model. + # ## Document types + # Document types are protocol-agnostic view of untyped data. They could be combined + # with a schema to serialize its contents. + # + # Smithy-Ruby currently only support JSON documents. class Document # @param [Object] data document data # @param [Hash] options diff --git a/gems/smithy-schema/lib/smithy-schema/type_registry.rb b/gems/smithy-schema/lib/smithy-schema/type_registry.rb index ffefa9d88..69e5e4e09 100644 --- a/gems/smithy-schema/lib/smithy-schema/type_registry.rb +++ b/gems/smithy-schema/lib/smithy-schema/type_registry.rb @@ -77,6 +77,7 @@ def convert_as_typed(document) end class << self + # Composes multiple type registries together. # @param [Array] # @return [TypeRegistry] def compose(*type_registries) From 6283813c30adf4011c1ea322a8b25266121922d6 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 14 Apr 2025 13:36:17 -0700 Subject: [PATCH 16/54] Add TypeRegistry specs --- .../lib/smithy-schema/type_registry.rb | 6 +- .../spec/smithy-schema/type_registry_spec.rb | 142 ++++++++++++++++++ 2 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 gems/smithy-schema/spec/smithy-schema/type_registry_spec.rb diff --git a/gems/smithy-schema/lib/smithy-schema/type_registry.rb b/gems/smithy-schema/lib/smithy-schema/type_registry.rb index 69e5e4e09..a72b7ea6d 100644 --- a/gems/smithy-schema/lib/smithy-schema/type_registry.rb +++ b/gems/smithy-schema/lib/smithy-schema/type_registry.rb @@ -23,7 +23,7 @@ def register(*shapes) shapes.each do |s| @registry[s.id] = s - case s.class + case s when Shapes::StructureShape @schema_by_types[s.type] = s if s.type when Shapes::UnionShape @@ -50,14 +50,14 @@ def schema_by_id(id) # with the given typed shape. # @param [Class] type # @return [Boolean] - def scheme_by_type?(type) + def schema_by_type?(type) @schema_by_types.key?(type) end # Returns the shape schema registered for the given typed shape. # @param [Class] type # @return [Shapes::Shape, nil] - def scheme_by_type(type) + def schema_by_type(type) @schema_by_types[type] end diff --git a/gems/smithy-schema/spec/smithy-schema/type_registry_spec.rb b/gems/smithy-schema/spec/smithy-schema/type_registry_spec.rb new file mode 100644 index 000000000..af0525908 --- /dev/null +++ b/gems/smithy-schema/spec/smithy-schema/type_registry_spec.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +require_relative '../spec_helper' + +module Smithy + module Schema + describe TypeRegistry do + subject do + registry = TypeRegistry.new + registry.register(shape) + registry + end + + let(:runtime_shape) do + Struct.new(:string, keyword_init: true) do + include Smithy::Schema::Structure + end + end + + let(:fake_shape) do + Struct.new(:foo, keyword_init: true) do + include Smithy::Schema::Structure + end + end + + let(:shape) do + shape = Shapes::StructureShape.new(id: 'thing') + string = Shapes::StringShape.new(id: 'smithy.api#String') + shape.add_member(:string, 'stringMember', string) + shape.type = runtime_shape + shape + end + + describe '#initialize' do + subject { TypeRegistry.new } + it 'defaults to empty registry' do + expect(subject.registry).to be_empty + end + end + + describe '#register' do + it 'register a schema' do + subject.register(Shapes::StructureShape.new(id: 'thing2')) + expect(subject.registry).to include('thing2') + end + + it 'register an array of schemas' do + subject.register( + Shapes::StructureShape.new(id: 'thing2'), + Shapes::StructureShape.new(id: 'thing3') + ) + expect(subject.registry).to include('thing2', 'thing3') + end + + it 'raises when invalid input is given' do + expect do + subject.register(1, 2) + end.to raise_error(ArgumentError) + end + end + + describe '#schema_by_id?' do + it 'returns true if registered' do + expect(subject.schema_by_id?('thing')).to be true + end + + it 'returns false if not registered' do + expect(subject.schema_by_id?('unknown')).to be false + end + end + + describe '#schema_by_id' do + it 'returns schema' do + expect(subject.schema_by_id('thing')).to be(shape) + end + + it 'returns nil if schema is not found' do + expect(subject.schema_by_id('unknown')).to be_nil + end + end + + describe '#schema_by_type?' do + it 'returns true if registered' do + expect(subject.schema_by_type?(runtime_shape)).to be true + end + + it 'returns false if not registered' do + expect(subject.schema_by_type?(fake_shape)).to be false + end + end + + describe '#schema_by_type' do + it 'returns schema' do + expect(subject.schema_by_type(runtime_shape)).to be(shape) + end + + it 'returns nil if schema is not found' do + expect(subject.schema_by_type(fake_shape)).to be_nil + end + end + + describe '#convert_as_typed' do + it 'returns a typed shape' do + document = Document.new(runtime_shape.new(string: 'foo'), schema: shape) + typed_shape = subject.convert_as_typed(document) + expect(typed_shape).to be_a(runtime_shape) + expect(typed_shape[:string]).to eq('foo') + end + + it 'raises when given document does not have a discriminator' do + expect do + subject.convert_as_typed(Document.new('foo')) + end.to raise_error(ArgumentError) + end + + it 'raises when given document discriminator is not found' do + shape = Shapes::StructureShape.new(id: 'thing2') + shape.type = runtime_shape + doc = Document.new(runtime_shape.new(string: 'foo'), schema: shape) + expect do + subject.convert_as_typed(doc) + end.to raise_error(ArgumentError) + end + end + + describe '.compose' do + it 'returns a combined registry' do + registry = TypeRegistry.new + registry.register(Shapes::StructureShape.new(id: 'thing2')) + new_registry = TypeRegistry.compose(subject, registry) + expect(new_registry.registry).to include('thing', 'thing2') + end + + it 'raises when invalid input is given' do + expect do + TypeRegistry.compose(1, 2) + end.to raise_error(ArgumentError) + end + end + end + end +end From 88ff845b2ab0293a264f2178f2de4e4f15118a74 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 14 Apr 2025 20:08:53 -0700 Subject: [PATCH 17/54] Add TypeRegistry tests --- gems/smithy-client/lib/smithy-client/base.rb | 10 ++++++++++ .../spec/smithy-client/base_spec.rb | 14 ++++++++++++++ .../lib/smithy/templates/client/client.erb | 1 + .../lib/smithy/templates/client/schema.erb | 3 ++- gems/smithy/lib/smithy/views/client/schema.rb | 4 ++++ .../spec/support/examples/schema_examples.rb | 19 +++++++++++++++++++ 6 files changed, 50 insertions(+), 1 deletion(-) diff --git a/gems/smithy-client/lib/smithy-client/base.rb b/gems/smithy-client/lib/smithy-client/base.rb index 902e02acd..3f9edde3c 100644 --- a/gems/smithy-client/lib/smithy-client/base.rb +++ b/gems/smithy-client/lib/smithy-client/base.rb @@ -161,6 +161,16 @@ def service=(service) define_operation_methods end + # @return [Schema::TypeRegistry] + def type_registry + @type_registry ||= Schema::TypeRegistry.new + end + + # @param [TypeRegistry] registry + def type_registry=(registry) + @type_registry = registry + end + # @option options [ServiceShape] :service (ServiceShape.new) # @option options [Array] :plugins ([]) A list of plugins to # add to the client class created. diff --git a/gems/smithy-client/spec/smithy-client/base_spec.rb b/gems/smithy-client/spec/smithy-client/base_spec.rb index e0574e165..0dba7cdb8 100644 --- a/gems/smithy-client/spec/smithy-client/base_spec.rb +++ b/gems/smithy-client/spec/smithy-client/base_spec.rb @@ -251,6 +251,20 @@ module Client end end + describe '.type_registry' do + it 'defaults to a TypeRegistry' do + expect(client_class.type_registry).to be_kind_of(Schema::TypeRegistry) + end + end + + describe '.type_registry=' do + it 'can be set' do + registry = Schema::TypeRegistry.new + client_class.type_registry = registry + expect(client_class.type_registry).to be(registry) + end + end + describe '.define' do it 'creates a new client class' do client_class = Base.define diff --git a/gems/smithy/lib/smithy/templates/client/client.erb b/gems/smithy/lib/smithy/templates/client/client.erb index 2fdd73b9b..03c1aeb66 100644 --- a/gems/smithy/lib/smithy/templates/client/client.erb +++ b/gems/smithy/lib/smithy/templates/client/client.erb @@ -13,6 +13,7 @@ module <%= module_name %> include Smithy::Client::Stubs self.service = Schema::SERVICE + self.type_registry = Schema::TYPE_REGISTRY <% add_plugins.each do |plugin_class| -%> add_plugin(<%= plugin_class %>) diff --git a/gems/smithy/lib/smithy/templates/client/schema.erb b/gems/smithy/lib/smithy/templates/client/schema.erb index 64248c238..5ec5ed694 100644 --- a/gems/smithy/lib/smithy/templates/client/schema.erb +++ b/gems/smithy/lib/smithy/templates/client/schema.erb @@ -46,7 +46,8 @@ module <%= module_name %> end) <% end -%> end + TYPE_REGISTRY = Smithy::Schema::TypeRegistry.new - TYPE_REGISTRY.register(<% shapes.each do |s| %><%= s.name %>, <% end %>) + TYPE_REGISTRY.register(<%= typed_shapes.map { |s| s.name }.join(', ')%>) end end diff --git a/gems/smithy/lib/smithy/views/client/schema.rb b/gems/smithy/lib/smithy/views/client/schema.rb index c5262d528..dadb01740 100644 --- a/gems/smithy/lib/smithy/views/client/schema.rb +++ b/gems/smithy/lib/smithy/views/client/schema.rb @@ -81,6 +81,10 @@ def shapes_with_members @shapes.select { |s| complex.include?(s.type) } end + def typed_shapes + @shapes.select(&:typed) + end + def operation_shapes @service_index .operations_for(@plan.service) diff --git a/gems/smithy/spec/support/examples/schema_examples.rb b/gems/smithy/spec/support/examples/schema_examples.rb index 3d09521e3..650d51e86 100644 --- a/gems/smithy/spec/support/examples/schema_examples.rb +++ b/gems/smithy/spec/support/examples/schema_examples.rb @@ -390,4 +390,23 @@ def expect_generated_shape(subject, shape_class, shape_hash) end end end + + context 'type registry' do + subject { ShapeService::Schema::TYPE_REGISTRY } + + let(:typed_shapes) do + fixture['shapes'].select do |_k, v| + %w[union structure].include?(v['type']) && + !v['traits']&.include?('smithy.api#trait') + end + end + + it 'generates a type registry' do + expect(subject).to be_a(Smithy::Schema::TypeRegistry) + end + + it 'contains a registry of typed shapes' do + expect(subject.registry.keys).to match_array(typed_shapes.keys) + end + end end From 66b2cde16c40cedcb2d2b6ac68c8ee03f9ff675a Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 14 Apr 2025 20:09:04 -0700 Subject: [PATCH 18/54] Update projections --- projections/shapes/lib/shapes/client.rb | 1 + projections/shapes/lib/shapes/schema.rb | 3 ++- projections/weather/lib/weather/client.rb | 1 + projections/weather/lib/weather/schema.rb | 3 ++- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/projections/shapes/lib/shapes/client.rb b/projections/shapes/lib/shapes/client.rb index a52335dc6..e126840f0 100644 --- a/projections/shapes/lib/shapes/client.rb +++ b/projections/shapes/lib/shapes/client.rb @@ -25,6 +25,7 @@ class Client < Smithy::Client::Base include Smithy::Client::Stubs self.service = Schema::SERVICE + self.type_registry = Schema::TYPE_REGISTRY add_plugin(ShapeService::Plugins::Auth) add_plugin(ShapeService::Plugins::Endpoint) diff --git a/projections/shapes/lib/shapes/schema.rb b/projections/shapes/lib/shapes/schema.rb index ad7db01c9..02fd940ef 100644 --- a/projections/shapes/lib/shapes/schema.rb +++ b/projections/shapes/lib/shapes/schema.rb @@ -74,7 +74,8 @@ module Schema operation.traits = {"smithy.ruby.tests#shape"=>{}} end) end + TYPE_REGISTRY = Smithy::Schema::TypeRegistry.new - TYPE_REGISTRY.register(BigDecimal, BigInteger, Blob, Boolean, Byte, Document, Double, Enum, Float, IntEnum, Integer, List, Long, Map, OperationInputOutput, Short, String, Structure, Timestamp, Union, ) + TYPE_REGISTRY.register(OperationInputOutput, Structure, Union) end end diff --git a/projections/weather/lib/weather/client.rb b/projections/weather/lib/weather/client.rb index beb259d4f..9fa2c1c4a 100644 --- a/projections/weather/lib/weather/client.rb +++ b/projections/weather/lib/weather/client.rb @@ -25,6 +25,7 @@ class Client < Smithy::Client::Base include Smithy::Client::Stubs self.service = Schema::SERVICE + self.type_registry = Schema::TYPE_REGISTRY add_plugin(Weather::Plugins::Auth) add_plugin(Weather::Plugins::Endpoint) diff --git a/projections/weather/lib/weather/schema.rb b/projections/weather/lib/weather/schema.rb index a1aa9ffd7..2e4d2bb16 100644 --- a/projections/weather/lib/weather/schema.rb +++ b/projections/weather/lib/weather/schema.rb @@ -83,7 +83,8 @@ module Schema operation[:paginator] = Paginators::ListCities.new end) end + TYPE_REGISTRY = Smithy::Schema::TypeRegistry.new - TYPE_REGISTRY.register(CityCoordinates, CityId, CitySummaries, CitySummary, GetCityInput, GetCityOutput, GetCurrentTimeOutput, GetForecastInput, GetForecastOutput, ListCitiesInput, ListCitiesOutput, NoSuchResource) + TYPE_REGISTRY.register(CityCoordinates, CitySummary, GetCityInput, GetCityOutput, GetCurrentTimeOutput, GetForecastInput, GetForecastOutput, ListCitiesInput, ListCitiesOutput, NoSuchResource) end end From 22998a0b78902f4d86d0b70470dcb315e4676712 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 14 Apr 2025 20:18:26 -0700 Subject: [PATCH 19/54] Update syntax --- gems/smithy-client/lib/smithy-client/base.rb | 2 +- gems/smithy-client/spec/smithy-client/base_spec.rb | 4 ++-- gems/smithy/lib/smithy/templates/client/client.erb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gems/smithy-client/lib/smithy-client/base.rb b/gems/smithy-client/lib/smithy-client/base.rb index 3f9edde3c..50613e96e 100644 --- a/gems/smithy-client/lib/smithy-client/base.rb +++ b/gems/smithy-client/lib/smithy-client/base.rb @@ -167,7 +167,7 @@ def type_registry end # @param [TypeRegistry] registry - def type_registry=(registry) + def set_type_registry=(registry) @type_registry = registry end diff --git a/gems/smithy-client/spec/smithy-client/base_spec.rb b/gems/smithy-client/spec/smithy-client/base_spec.rb index 0dba7cdb8..465ac7659 100644 --- a/gems/smithy-client/spec/smithy-client/base_spec.rb +++ b/gems/smithy-client/spec/smithy-client/base_spec.rb @@ -257,10 +257,10 @@ module Client end end - describe '.type_registry=' do + describe '.set_type_registry' do it 'can be set' do registry = Schema::TypeRegistry.new - client_class.type_registry = registry + client_class.set_type_registry = registry expect(client_class.type_registry).to be(registry) end end diff --git a/gems/smithy/lib/smithy/templates/client/client.erb b/gems/smithy/lib/smithy/templates/client/client.erb index 03c1aeb66..e8061f911 100644 --- a/gems/smithy/lib/smithy/templates/client/client.erb +++ b/gems/smithy/lib/smithy/templates/client/client.erb @@ -13,7 +13,7 @@ module <%= module_name %> include Smithy::Client::Stubs self.service = Schema::SERVICE - self.type_registry = Schema::TYPE_REGISTRY + self.set_type_registry = Schema::TYPE_REGISTRY <% add_plugins.each do |plugin_class| -%> add_plugin(<%= plugin_class %>) From 9afeacd852c31e17584f39cba48751869b73f603 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 14 Apr 2025 20:18:57 -0700 Subject: [PATCH 20/54] Update projections --- projections/shapes/lib/shapes/client.rb | 2 +- projections/weather/lib/weather/client.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/projections/shapes/lib/shapes/client.rb b/projections/shapes/lib/shapes/client.rb index e126840f0..6ed8debfc 100644 --- a/projections/shapes/lib/shapes/client.rb +++ b/projections/shapes/lib/shapes/client.rb @@ -25,7 +25,7 @@ class Client < Smithy::Client::Base include Smithy::Client::Stubs self.service = Schema::SERVICE - self.type_registry = Schema::TYPE_REGISTRY + self.set_type_registry = Schema::TYPE_REGISTRY add_plugin(ShapeService::Plugins::Auth) add_plugin(ShapeService::Plugins::Endpoint) diff --git a/projections/weather/lib/weather/client.rb b/projections/weather/lib/weather/client.rb index 9fa2c1c4a..9eb3ad1b0 100644 --- a/projections/weather/lib/weather/client.rb +++ b/projections/weather/lib/weather/client.rb @@ -25,7 +25,7 @@ class Client < Smithy::Client::Base include Smithy::Client::Stubs self.service = Schema::SERVICE - self.type_registry = Schema::TYPE_REGISTRY + self.set_type_registry = Schema::TYPE_REGISTRY add_plugin(Weather::Plugins::Auth) add_plugin(Weather::Plugins::Endpoint) From e8920fdb4d57ed5f4173884aa8392f212bde74bc Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Thu, 17 Apr 2025 12:58:06 -0700 Subject: [PATCH 21/54] Change schema name to shapes to stay aligned --- .../lib/smithy-schema/document.rb | 44 +++++----- .../lib/smithy-schema/document_utils.rb | 88 +++++++++---------- .../lib/smithy-schema/type_registry.rb | 36 ++++---- .../spec/smithy-schema/document_spec.rb | 6 +- .../spec/smithy-schema/type_registry_spec.rb | 40 ++++----- 5 files changed, 107 insertions(+), 107 deletions(-) diff --git a/gems/smithy-schema/lib/smithy-schema/document.rb b/gems/smithy-schema/lib/smithy-schema/document.rb index de4720994..404fc18a9 100644 --- a/gems/smithy-schema/lib/smithy-schema/document.rb +++ b/gems/smithy-schema/lib/smithy-schema/document.rb @@ -7,19 +7,19 @@ module Schema # A Smithy document type, representing typed or untyped data from Smithy data model. # ## Document types # Document types are protocol-agnostic view of untyped data. They could be combined - # with a schema to serialize its contents. + # with a shape to serialize its contents. # # Smithy-Ruby currently only support JSON documents. class Document # @param [Object] data document data # @param [Hash] options - # @option options [Smithy::Schema::Structure] :schema schema to reference when setting + # @option options [Smithy::Schema::Structure] :shape shape to reference when setting # document data. Only applicable when data param is a type of {Shapes::StructureShape}. # @option options [Boolean] :use_timestamp_format Whether to use the `timestampFormat` - # trait or ignore it when creating a {Document} with given schema. The `timestampFormat` + # trait or ignore it when creating a {Document} with given shape. The `timestampFormat` # trait is ignored by default. # @option options [Boolean] :use_json_name Whether to use the `jsonName` trait or ignore - # it when creating a {Document} with given schema. The `jsonName` trait is ignored + # it when creating a {Document} with given shape. The `jsonName` trait is ignored # by default. def initialize(data, options = {}) @data = set_data(data, options) @@ -40,14 +40,14 @@ def [](key) @data[key] end - # @param [Shapes::Shape] schema + # @param [Shapes::Shape] shape # @return [Shapes::Structure] typed shape - def as_typed(schema) - error_message = 'Invalid schema or document data' - raise ArgumentError, error_message unless valid_schema?(schema) && @data.is_a?(Hash) + def as_typed(shape) + error_message = 'Invalid shape or document data' + raise ArgumentError, error_message unless valid_shape?(shape) && @data.is_a?(Hash) - type = schema.type.new - DocumentUtils.apply(@data, schema, type) + type = shape.type.new + DocumentUtils.apply(@data, shape, type) end private @@ -59,15 +59,15 @@ def discriminator?(data) def extract_discriminator(data, opts) return if data.nil? - return unless discriminator?(data) || (schema = opts[:schema]) + return unless discriminator?(data) || (shape = opts[:shape]) if discriminator?(data) data['__type'] else - error_message = "Expected a structure schema, given #{schema.class} instead" - raise error_message unless valid_schema?(schema) + error_message = "Expected a structure shape, given #{shape.class} instead" + raise error_message unless valid_shape?(shape) - schema.id + shape.id end end @@ -76,14 +76,14 @@ def set_data(data, opts) case data when Smithy::Schema::Structure - schema = opts[:schema] - if schema.nil? || !valid_schema?(schema) - raise ArgumentError, "Unable to create a document with given schema: #{schema}" + shape = opts[:shape] + if shape.nil? || !valid_shape?(shape) + raise ArgumentError, "Unable to create a document with given shape: #{shape}" end - opts = opts.except(:schema) - # case 1 - extract data from runtime shape, schema is required to know to properly extract - DocumentUtils.extract(data, schema, opts) + opts = opts.except(:shape) + # case 1 - extract data from runtime shape, shape is required to know to properly extract + DocumentUtils.extract(data, shape, opts) else if discriminator?(data) @@ -96,8 +96,8 @@ def set_data(data, opts) end end - def valid_schema?(schema) - schema.is_a?(Shapes::StructureShape) && !schema.type.nil? + def valid_shape?(shape) + shape.is_a?(Shapes::StructureShape) && !shape.type.nil? end end end diff --git a/gems/smithy-schema/lib/smithy-schema/document_utils.rb b/gems/smithy-schema/lib/smithy-schema/document_utils.rb index 726d0b47f..ea6aab23a 100644 --- a/gems/smithy-schema/lib/smithy-schema/document_utils.rb +++ b/gems/smithy-schema/lib/smithy-schema/document_utils.rb @@ -26,29 +26,29 @@ def format(data) end # Used to apply data to runtime shape - def apply(data, schema, type = nil) - case shape(schema) - when Shapes::StructureShape then apply_structure(data, schema, type) - when Shapes::UnionShape then apply_union(data, schema, type) - when Shapes::ListShape then apply_list(data, schema) - when Shapes::MapShape then apply_map(data, schema) - when Shapes::TimestampShape then apply_timestamp(data, schema) + def apply(data, shape, type = nil) + case shape_reference(shape) + when Shapes::StructureShape then apply_structure(data, shape, type) + when Shapes::UnionShape then apply_union(data, shape, type) + when Shapes::ListShape then apply_list(data, shape) + when Shapes::MapShape then apply_map(data, shape) + when Shapes::TimestampShape then apply_timestamp(data, shape) when Shapes::BlobShape then Base64.decode64(data) else data end end # rubocop:disable Metrics/CyclomaticComplexity - def extract(data, schema, opts = {}) + def extract(data, shape, opts = {}) return if data.nil? - case shape(schema) - when Shapes::StructureShape then extract_structure(data, schema, opts) - when Shapes::UnionShape then extract_union(data, schema, opts) - when Shapes::ListShape then extract_list(data, schema) - when Shapes::MapShape then extract_map(data, schema) + case shape_reference(shape) + when Shapes::StructureShape then extract_structure(data, shape, opts) + when Shapes::UnionShape then extract_union(data, shape, opts) + when Shapes::ListShape then extract_list(data, shape) + when Shapes::MapShape then extract_map(data, shape) when Shapes::BlobShape then extract_blob(data) - when Shapes::TimestampShape then extract_timestamp(data, schema, opts) + when Shapes::TimestampShape then extract_timestamp(data, shape, opts) else data end end @@ -56,8 +56,8 @@ def extract(data, schema, opts = {}) private - def apply_list(data, schema) - shape = shape(schema) + def apply_list(data, shape) + shape = shape_reference(shape) data.map do |v| next if v.nil? @@ -65,8 +65,8 @@ def apply_list(data, schema) end end - def apply_map(data, schema) - shape = shape(schema) + def apply_map(data, shape) + shape = shape_reference(shape) data.transform_values do |v| if v.nil? nil @@ -76,8 +76,8 @@ def apply_map(data, schema) end end - def apply_structure(data, schema, type) - shape = shape(schema) + def apply_structure(data, shape, type) + shape = shape_reference(shape) type = shape.type.new if type.nil? data.each do |k, v| @@ -94,13 +94,13 @@ def apply_structure(data, schema, type) type end - def apply_timestamp(data, schema) + def apply_timestamp(data, shape) data = data.is_a?(Numeric) ? Time.at(data) : Time.parse(data) - time(data, timestamp_format(schema)) + time(data, timestamp_format(shape)) end - def apply_union(data, schema, type) - shape = shape(schema) + def apply_union(data, shape, type) + shape = shape_reference(shape) key, value = data.flatten return if key.nil? @@ -123,18 +123,18 @@ def extract_blob(data) Base64.strict_encode64(data.is_a?(String) ? data : data.read) end - def extract_list(data, schema) - shape = shape(schema) + def extract_list(data, shape) + shape = shape_reference(shape) data.collect { |v| extract(v, shape.member) } end - def extract_map(data, schema) - shape = shape(schema) + def extract_map(data, shape) + shape = shape_reference(shape) data.each.with_object({}) { |(k, v), h| h[k] = extract(v, shape.value) } end - def extract_structure(data, schema, opts) - shape = shape(schema) + def extract_structure(data, shape, opts) + shape = shape_reference(shape) data.to_h.each_with_object({}) do |(k, v), o| next unless shape.member?(k) @@ -144,17 +144,17 @@ def extract_structure(data, schema, opts) end end - def extract_timestamp(data, schema, opts) + def extract_timestamp(data, shape, opts) return unless data.is_a?(Time) - trait = timestamp_format(schema) if opts[:use_timestamp_format] + trait = timestamp_format(shape) if opts[:use_timestamp_format] time(data, trait) end # rubocop:disable Metrics/AbcSize - def extract_union(data, schema, opts) + def extract_union(data, shape, opts) h = {} - shape = shape(schema) + shape = shape_reference(shape) if data.is_a?(Schema::Union) member_shape = shape.member_by_type(data.class) member_name = resolve_member_name(member_shape, opts) @@ -171,10 +171,10 @@ def extract_union(data, schema, opts) end # rubocop:enable Metrics/AbcSize - def member_name(schema, key) - return unless schema.name_by_member_name?(key) || schema.member?(key.to_sym) + def member_name(shape, key) + return unless shape.name_by_member_name?(key) || shape.member?(key.to_sym) - schema.name_by_member_name(key) || key.to_sym + shape.name_by_member_name(key) || key.to_sym end def member_with_json_name(name, shape) @@ -191,19 +191,19 @@ def resolve_member_name(member_shape, opts) end end - def shape(schema) - schema.is_a?(Shapes::MemberShape) ? schema.shape : schema + def shape_reference(shape) + shape.is_a?(Shapes::MemberShape) ? shape.shape : shape end # The following steps are taken to determine the format of timestamp: # Use the timestampFormat trait of the member, if present. # Use the timestampFormat trait of the shape, if present. # If none of the above applies, use epoch-seconds as default - def timestamp_format(schema) - if schema.traits['smithy.api#timestampFormat'] - schema.traits['smithy.api#timestampFormat'] - elsif schema.shape.traits['smithy.api#timestampFormat'] - schema.shape.traits['smithy.api#timestampFormat'] + def timestamp_format(shape) + if shape.traits['smithy.api#timestampFormat'] + shape.traits['smithy.api#timestampFormat'] + elsif shape.shape.traits['smithy.api#timestampFormat'] + shape.shape.traits['smithy.api#timestampFormat'] else 'epoch-seconds' end diff --git a/gems/smithy-schema/lib/smithy-schema/type_registry.rb b/gems/smithy-schema/lib/smithy-schema/type_registry.rb index a72b7ea6d..a83fce87e 100644 --- a/gems/smithy-schema/lib/smithy-schema/type_registry.rb +++ b/gems/smithy-schema/lib/smithy-schema/type_registry.rb @@ -2,16 +2,16 @@ module Smithy module Schema - # A registry that contains a map of Smithy shape ID to its schema. - # Also includes a way to find schema based on its shape type representation + # A registry that contains a map of Smithy shape ID to its shape. + # Also includes a way to find shape based on its type representation class TypeRegistry def initialize @registry = {} - @schema_by_types = {} + @shape_by_types = {} end # @api private - attr_reader :schema_by_types + attr_reader :shape_by_types # @return [Hash] attr_accessor :registry @@ -25,9 +25,9 @@ def register(*shapes) case s when Shapes::StructureShape - @schema_by_types[s.type] = s if s.type + @shape_by_types[s.type] = s if s.type when Shapes::UnionShape - s.member_types.values { |v| @schema_by_types[v] = s } + s.member_types.values { |v| @shape_by_types[v] = s } end end end @@ -35,30 +35,30 @@ def register(*shapes) # Returns true if this type registry contains specific shape id. # @param [String] id # @return [Boolean] - def schema_by_id?(id) + def shape_by_id?(id) @registry.key?(id) end - # Returns the shape schema registered for the given shape id. + # Returns the shape registered for the given shape id. # @param [id] id # @return [Shapes::Shape, nil] - def schema_by_id(id) + def shape_by_id(id) @registry[id] end - # Returns true if this type registry contains a schema associated + # Returns true if this type registry contains a shape associated # with the given typed shape. # @param [Class] type # @return [Boolean] - def schema_by_type?(type) - @schema_by_types.key?(type) + def shape_by_type?(type) + @shape_by_types.key?(type) end - # Returns the shape schema registered for the given typed shape. + # Returns the shape shape registered for the given typed shape. # @param [Class] type # @return [Shapes::Shape, nil] - def schema_by_type(type) - @schema_by_types[type] + def shape_by_type(type) + @shape_by_types[type] end # Deserializes a document into a typed shape from registry. @@ -68,10 +68,10 @@ def convert_as_typed(document) msg = 'Unable to convert given document since discriminator is not set' raise ArgumentError, msg unless document.discriminator - if schema_by_id?(document.discriminator) - document.as_typed(schema_by_id(document.discriminator)) + if shape_by_id?(document.discriminator) + document.as_typed(shape_by_id(document.discriminator)) else - msg = "Unable to find schema with #{document.discriminator} in Registry" + msg = "Unable to find shape with #{document.discriminator} in Registry" raise ArgumentError, msg end end diff --git a/gems/smithy-schema/spec/smithy-schema/document_spec.rb b/gems/smithy-schema/spec/smithy-schema/document_spec.rb index 9064631e2..da3ef8b78 100644 --- a/gems/smithy-schema/spec/smithy-schema/document_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/document_spec.rb @@ -138,7 +138,7 @@ def self.name ) end - subject { Document.new(typed_shape, schema: schema) } + subject { Document.new(typed_shape, shape: schema) } describe '#initialize' do it 'set data' do @@ -157,12 +157,12 @@ def self.name it 'set data using jsonName when applicable' do typed_shape = runtime.new(string: 'foo', union: { union_string: 'bar' }) - doc = Document.new(typed_shape, schema: schema, use_json_name: true) + doc = Document.new(typed_shape, shape: schema, use_json_name: true) expect(doc.data).to include({ 'json' => 'foo', 'unionMember' => { 'json' => 'bar' } }) end it 'set data using timestampTrait when applicable' do - doc = Document.new(typed_shape, schema: schema, use_timestamp_format: true) + doc = Document.new(typed_shape, shape: schema, use_timestamp_format: true) expect(doc.data['timeMember']).to eq('2024-12-25T00:00:00Z') end diff --git a/gems/smithy-schema/spec/smithy-schema/type_registry_spec.rb b/gems/smithy-schema/spec/smithy-schema/type_registry_spec.rb index af0525908..5bc3bfa9a 100644 --- a/gems/smithy-schema/spec/smithy-schema/type_registry_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/type_registry_spec.rb @@ -39,12 +39,12 @@ module Schema end describe '#register' do - it 'register a schema' do + it 'register a shape' do subject.register(Shapes::StructureShape.new(id: 'thing2')) expect(subject.registry).to include('thing2') end - it 'register an array of schemas' do + it 'register an array of shapes' do subject.register( Shapes::StructureShape.new(id: 'thing2'), Shapes::StructureShape.new(id: 'thing3') @@ -59,49 +59,49 @@ module Schema end end - describe '#schema_by_id?' do + describe '#shape_by_id?' do it 'returns true if registered' do - expect(subject.schema_by_id?('thing')).to be true + expect(subject.shape_by_id?('thing')).to be true end it 'returns false if not registered' do - expect(subject.schema_by_id?('unknown')).to be false + expect(subject.shape_by_id?('unknown')).to be false end end - describe '#schema_by_id' do - it 'returns schema' do - expect(subject.schema_by_id('thing')).to be(shape) + describe '#shape_by_id' do + it 'returns shape' do + expect(subject.shape_by_id('thing')).to be(shape) end - it 'returns nil if schema is not found' do - expect(subject.schema_by_id('unknown')).to be_nil + it 'returns nil if shape is not found' do + expect(subject.shape_by_id('unknown')).to be_nil end end - describe '#schema_by_type?' do + describe '#shape_by_type?' do it 'returns true if registered' do - expect(subject.schema_by_type?(runtime_shape)).to be true + expect(subject.shape_by_type?(runtime_shape)).to be true end it 'returns false if not registered' do - expect(subject.schema_by_type?(fake_shape)).to be false + expect(subject.shape_by_type?(fake_shape)).to be false end end - describe '#schema_by_type' do - it 'returns schema' do - expect(subject.schema_by_type(runtime_shape)).to be(shape) + describe '#shape_by_type' do + it 'returns shape' do + expect(subject.shape_by_type(runtime_shape)).to be(shape) end - it 'returns nil if schema is not found' do - expect(subject.schema_by_type(fake_shape)).to be_nil + it 'returns nil if shape is not found' do + expect(subject.shape_by_type(fake_shape)).to be_nil end end describe '#convert_as_typed' do it 'returns a typed shape' do - document = Document.new(runtime_shape.new(string: 'foo'), schema: shape) + document = Document.new(runtime_shape.new(string: 'foo'), shape: shape) typed_shape = subject.convert_as_typed(document) expect(typed_shape).to be_a(runtime_shape) expect(typed_shape[:string]).to eq('foo') @@ -116,7 +116,7 @@ module Schema it 'raises when given document discriminator is not found' do shape = Shapes::StructureShape.new(id: 'thing2') shape.type = runtime_shape - doc = Document.new(runtime_shape.new(string: 'foo'), schema: shape) + doc = Document.new(runtime_shape.new(string: 'foo'), shape: shape) expect do subject.convert_as_typed(doc) end.to raise_error(ArgumentError) From ef8c0271aee591705d790b17ea40412613ee45f4 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Thu, 17 Apr 2025 13:01:07 -0700 Subject: [PATCH 22/54] Remove as_typed method from TypeRegistry --- .../lib/smithy-schema/type_registry.rb | 15 ------------ .../spec/smithy-schema/type_registry_spec.rb | 24 ------------------- 2 files changed, 39 deletions(-) diff --git a/gems/smithy-schema/lib/smithy-schema/type_registry.rb b/gems/smithy-schema/lib/smithy-schema/type_registry.rb index a83fce87e..0e9425780 100644 --- a/gems/smithy-schema/lib/smithy-schema/type_registry.rb +++ b/gems/smithy-schema/lib/smithy-schema/type_registry.rb @@ -61,21 +61,6 @@ def shape_by_type(type) @shape_by_types[type] end - # Deserializes a document into a typed shape from registry. - # @param [Document] document - # @return [Shapes::Structure] typed shape - def convert_as_typed(document) - msg = 'Unable to convert given document since discriminator is not set' - raise ArgumentError, msg unless document.discriminator - - if shape_by_id?(document.discriminator) - document.as_typed(shape_by_id(document.discriminator)) - else - msg = "Unable to find shape with #{document.discriminator} in Registry" - raise ArgumentError, msg - end - end - class << self # Composes multiple type registries together. # @param [Array] diff --git a/gems/smithy-schema/spec/smithy-schema/type_registry_spec.rb b/gems/smithy-schema/spec/smithy-schema/type_registry_spec.rb index 5bc3bfa9a..60c4e1d84 100644 --- a/gems/smithy-schema/spec/smithy-schema/type_registry_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/type_registry_spec.rb @@ -99,30 +99,6 @@ module Schema end end - describe '#convert_as_typed' do - it 'returns a typed shape' do - document = Document.new(runtime_shape.new(string: 'foo'), shape: shape) - typed_shape = subject.convert_as_typed(document) - expect(typed_shape).to be_a(runtime_shape) - expect(typed_shape[:string]).to eq('foo') - end - - it 'raises when given document does not have a discriminator' do - expect do - subject.convert_as_typed(Document.new('foo')) - end.to raise_error(ArgumentError) - end - - it 'raises when given document discriminator is not found' do - shape = Shapes::StructureShape.new(id: 'thing2') - shape.type = runtime_shape - doc = Document.new(runtime_shape.new(string: 'foo'), shape: shape) - expect do - subject.convert_as_typed(doc) - end.to raise_error(ArgumentError) - end - end - describe '.compose' do it 'returns a combined registry' do registry = TypeRegistry.new From 385466142023ed190b55f41e9712452466943c3f Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Fri, 18 Apr 2025 09:42:32 -0700 Subject: [PATCH 23/54] Create TimeHelper module --- gems/smithy-schema/lib/smithy-schema.rb | 1 + .../lib/smithy-schema/document_utils.rb | 23 ++----------- .../lib/smithy-schema/time_helper.rb | 30 ++++++++++++++++ .../spec/smithy-schema/time_helper_spec.rb | 34 +++++++++++++++++++ 4 files changed, 68 insertions(+), 20 deletions(-) create mode 100644 gems/smithy-schema/lib/smithy-schema/time_helper.rb create mode 100644 gems/smithy-schema/spec/smithy-schema/time_helper_spec.rb diff --git a/gems/smithy-schema/lib/smithy-schema.rb b/gems/smithy-schema/lib/smithy-schema.rb index 55221cf63..e07cffe29 100644 --- a/gems/smithy-schema/lib/smithy-schema.rb +++ b/gems/smithy-schema/lib/smithy-schema.rb @@ -3,6 +3,7 @@ require_relative 'smithy-schema/shapes' require_relative 'smithy-schema/structure' require_relative 'smithy-schema/document' +require_relative 'smithy-schema/time_helper' require_relative 'smithy-schema/type_registry' require_relative 'smithy-schema/union' diff --git a/gems/smithy-schema/lib/smithy-schema/document_utils.rb b/gems/smithy-schema/lib/smithy-schema/document_utils.rb index ea6aab23a..587d7e794 100644 --- a/gems/smithy-schema/lib/smithy-schema/document_utils.rb +++ b/gems/smithy-schema/lib/smithy-schema/document_utils.rb @@ -96,7 +96,7 @@ def apply_structure(data, shape, type) def apply_timestamp(data, shape) data = data.is_a?(Numeric) ? Time.at(data) : Time.parse(data) - time(data, timestamp_format(shape)) + TimeHelper.time(data, timestamp_format(shape)) end def apply_union(data, shape, type) @@ -147,8 +147,8 @@ def extract_structure(data, shape, opts) def extract_timestamp(data, shape, opts) return unless data.is_a?(Time) - trait = timestamp_format(shape) if opts[:use_timestamp_format] - time(data, trait) + trait = opts[:use_timestamp_format] ? timestamp_format(shape) : 'epoch-seconds' + TimeHelper.time(data, trait) end # rubocop:disable Metrics/AbcSize @@ -208,23 +208,6 @@ def timestamp_format(shape) 'epoch-seconds' end end - - def time(data, trait = nil) - if trait - case trait - when 'http-date' - data.utc.iso8601 - when 'date-time' - data.utc.httpdate - when 'epoch-seconds' - data.utc.to_i - else - raise "unhandled timestamp format `#{value}`" - end - else - data.utc.to_i # default format - end - end end end end diff --git a/gems/smithy-schema/lib/smithy-schema/time_helper.rb b/gems/smithy-schema/lib/smithy-schema/time_helper.rb new file mode 100644 index 000000000..d8e4a535e --- /dev/null +++ b/gems/smithy-schema/lib/smithy-schema/time_helper.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Smithy + module Schema + # A module that provides helper methods to convert Time objects + # based on the given TimestampFormat trait. + # @api private + module TimeHelper + class << self + # @param [Time] time + # @param [String] trait TimestampFormat trait value + # @return [Object] The time as TimestampFormat trait format + def time(time, trait) + raise ArgumentError, 'expected Time as input' unless time.is_a?(Time) + + case trait + when 'http-date' + time.utc.iso8601 + when 'date-time' + time.utc.httpdate + when 'epoch-seconds' + time.utc.to_i + else + raise ArgumentError, "unhandled timestamp format `#{trait}`" + end + end + end + end + end +end diff --git a/gems/smithy-schema/spec/smithy-schema/time_helper_spec.rb b/gems/smithy-schema/spec/smithy-schema/time_helper_spec.rb new file mode 100644 index 000000000..1daf6a61b --- /dev/null +++ b/gems/smithy-schema/spec/smithy-schema/time_helper_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require_relative '../spec_helper' +require 'time' + +module Smithy + module Schema + describe TimeHelper do + describe '#time' do + let(:time) { Time.new(2002, 10, 31) } + + it 'returns as http-date format' do + expect(subject.time(time, 'http-date')).to eq('2002-10-31T08:00:00Z') + end + + it 'returns as date-time format' do + expect(subject.time(time, 'date-time')).to eq('Thu, 31 Oct 2002 08:00:00 GMT') + end + + it 'returns as epoch-seconds format' do + expect(subject.time(time, 'epoch-seconds')).to eq(1_036_051_200) + end + + it 'raises when given time is invalid ' do + expect { subject.time('time', 'http-date') }.to raise_error(ArgumentError) + end + + it 'raises when given timestamp trait is unhandled' do + expect { subject.time(time, 'foo') }.to raise_error(ArgumentError) + end + end + end + end +end From 48e1b0fa55fe4baac4e2c79320a1b561511fccb2 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Fri, 18 Apr 2025 09:48:03 -0700 Subject: [PATCH 24/54] Fix timestamp failures --- gems/smithy-schema/lib/smithy-schema/time_helper.rb | 1 + gems/smithy-schema/spec/smithy-schema/time_helper_spec.rb | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/gems/smithy-schema/lib/smithy-schema/time_helper.rb b/gems/smithy-schema/lib/smithy-schema/time_helper.rb index d8e4a535e..e17634503 100644 --- a/gems/smithy-schema/lib/smithy-schema/time_helper.rb +++ b/gems/smithy-schema/lib/smithy-schema/time_helper.rb @@ -5,6 +5,7 @@ module Schema # A module that provides helper methods to convert Time objects # based on the given TimestampFormat trait. # @api private + # TODO: need to handle fractional secs module TimeHelper class << self # @param [Time] time diff --git a/gems/smithy-schema/spec/smithy-schema/time_helper_spec.rb b/gems/smithy-schema/spec/smithy-schema/time_helper_spec.rb index 1daf6a61b..daa78d6f9 100644 --- a/gems/smithy-schema/spec/smithy-schema/time_helper_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/time_helper_spec.rb @@ -7,18 +7,18 @@ module Smithy module Schema describe TimeHelper do describe '#time' do - let(:time) { Time.new(2002, 10, 31) } + let(:time) { Time.utc(2002, 10, 31) } it 'returns as http-date format' do - expect(subject.time(time, 'http-date')).to eq('2002-10-31T08:00:00Z') + expect(subject.time(time, 'http-date')).to eq('2002-10-31T00:00:00Z') end it 'returns as date-time format' do - expect(subject.time(time, 'date-time')).to eq('Thu, 31 Oct 2002 08:00:00 GMT') + expect(subject.time(time, 'date-time')).to eq('Thu, 31 Oct 2002 00:00:00 GMT') end it 'returns as epoch-seconds format' do - expect(subject.time(time, 'epoch-seconds')).to eq(1_036_051_200) + expect(subject.time(time, 'epoch-seconds')).to eq(1_036_022_400) end it 'raises when given time is invalid ' do From 04d0d5b66724cf532803c03d885db8eaf35d7979 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 21 Apr 2025 09:39:21 -0700 Subject: [PATCH 25/54] Refactor type registry per feedbacks --- .../lib/smithy-schema/type_registry.rb | 98 +++++++++++-------- .../spec/smithy-schema/type_registry_spec.rb | 68 +++++++------ .../lib/smithy/templates/client/schema.erb | 3 +- gems/smithy/lib/smithy/views/client/schema.rb | 2 +- 4 files changed, 91 insertions(+), 80 deletions(-) diff --git a/gems/smithy-schema/lib/smithy-schema/type_registry.rb b/gems/smithy-schema/lib/smithy-schema/type_registry.rb index 0e9425780..852e7c35b 100644 --- a/gems/smithy-schema/lib/smithy-schema/type_registry.rb +++ b/gems/smithy-schema/lib/smithy-schema/type_registry.rb @@ -3,75 +3,89 @@ module Smithy module Schema # A registry that contains a map of Smithy shape ID to its shape. - # Also includes a way to find shape based on its type representation + # Also includes a way to find shape based on its type representation. class TypeRegistry - def initialize - @registry = {} - @shape_by_types = {} + include Enumerable + def initialize(registry = {}) + @registry = registry + @shapes_by_type = register_shape_types(registry.values) end # @api private - attr_reader :shape_by_types - - # @return [Hash] + # @return [Hash] attr_accessor :registry - # @param [Array] shapes - def register(*shapes) - raise ArgumentError, 'Expected an array of Shapes' unless shapes.all?(Shapes::Shape) + # @api private + # @return [Hash] + attr_reader :shapes_by_type - shapes.each do |s| - @registry[s.id] = s + # @return [Hash] + def each(&) + @registry.each(&) + end - case s - when Shapes::StructureShape - @shape_by_types[s.type] = s if s.type - when Shapes::UnionShape - s.member_types.values { |v| @shape_by_types[v] = s } - end - end + # @param [String] id + # @return [Shapes::Structure, nil] + def [](id) + @registry[id] end - # Returns true if this type registry contains specific shape id. + def []=(id, shape) + msg = 'Expected a shape with members and type' + raise ArgumentError, msg unless shape.is_a?(Shapes::Structure) && shape.type + + @registry[id] = shape + register_shape_type(shape, @shapes_by_type) + end + + # Returns true if the registry contains specific shape id. # @param [String] id # @return [Boolean] - def shape_by_id?(id) + def key?(id) @registry.key?(id) end - # Returns the shape registered for the given shape id. - # @param [id] id - # @return [Shapes::Shape, nil] - def shape_by_id(id) - @registry[id] - end - - # Returns true if this type registry contains a shape associated - # with the given typed shape. - # @param [Class] type + # Returns true if the registry contains a shape associated + # with the given type. + # @param [Struct] type # @return [Boolean] def shape_by_type?(type) - @shape_by_types.key?(type) + @shapes_by_type.key?(type) end - # Returns the shape shape registered for the given typed shape. - # @param [Class] type - # @return [Shapes::Shape, nil] + # Returns the shape registered for the given type. + # @param [Struct] type + # @return [Shapes::Structure, nil] def shape_by_type(type) - @shape_by_types[type] + @shapes_by_type[type] + end + + private + + def register_shape_types(shapes) + shapes.each_with_object({}) do |s, h| + register_shape_type(s, h) + end + end + + def register_shape_type(shape, mapping) + case shape + when Shapes::StructureShape + mapping[shape.type] = shape + when Shapes::UnionShape + shape.member_types.values { |v| mapping[v] = shape } + end end class << self - # Composes multiple type registries together. + # Combines multiple registries and returns a new registry. # @param [Array] # @return [TypeRegistry] - def compose(*type_registries) + def concat(*type_registries) raise ArgumentError, 'Expected an array of TypeRegistries' unless type_registries.all?(self) - new_type_registry = new - new_type_registry.registry = - type_registries.each_with_object({}) { |r, h| h.merge!(r.registry) } - new_type_registry + combined_registry = type_registries.map(&:registry).reduce({}, :merge) + new(combined_registry) end end end diff --git a/gems/smithy-schema/spec/smithy-schema/type_registry_spec.rb b/gems/smithy-schema/spec/smithy-schema/type_registry_spec.rb index 60c4e1d84..36659ba12 100644 --- a/gems/smithy-schema/spec/smithy-schema/type_registry_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/type_registry_spec.rb @@ -5,11 +5,7 @@ module Smithy module Schema describe TypeRegistry do - subject do - registry = TypeRegistry.new - registry.register(shape) - registry - end + subject { TypeRegistry.new({ 'thing' => shape }) } let(:runtime_shape) do Struct.new(:string, keyword_init: true) do @@ -33,49 +29,51 @@ module Schema describe '#initialize' do subject { TypeRegistry.new } + it 'defaults to empty registry' do expect(subject.registry).to be_empty end end - describe '#register' do - it 'register a shape' do - subject.register(Shapes::StructureShape.new(id: 'thing2')) - expect(subject.registry).to include('thing2') + describe '#each' do + it 'is enumerable' do + expect(subject).to be_kind_of(Enumerable) end + end - it 'register an array of shapes' do - subject.register( - Shapes::StructureShape.new(id: 'thing2'), - Shapes::StructureShape.new(id: 'thing3') - ) - expect(subject.registry).to include('thing2', 'thing3') + describe '#[]' do + it 'returns shape' do + expect(subject['thing']).to be(shape) end - it 'raises when invalid input is given' do - expect do - subject.register(1, 2) - end.to raise_error(ArgumentError) + it 'returns nil if shape is not found' do + expect(subject['unknown']).to be_nil end end - describe '#shape_by_id?' do - it 'returns true if registered' do - expect(subject.shape_by_id?('thing')).to be true + describe '#[]=' do + it 'adds a shape' do + subject['thing2'] = shape + expect(subject.registry).to include('thing2') end - it 'returns false if not registered' do - expect(subject.shape_by_id?('unknown')).to be false + it 'raises when an invalid shape is given' do + expect do + subject['thing2'] = Shapes::StringShape.new + end.to raise_error(ArgumentError) + expect do + subject['thing2'] = Shapes::StructureShape.new + end.to raise_error(ArgumentError) end end - describe '#shape_by_id' do - it 'returns shape' do - expect(subject.shape_by_id('thing')).to be(shape) + describe '#key?' do + it 'returns true if shape is registered' do + expect(subject.key?('thing')).to be true end - it 'returns nil if shape is not found' do - expect(subject.shape_by_id('unknown')).to be_nil + it 'returns false if shape is not registered' do + expect(subject.key?('unknown')).to be false end end @@ -99,17 +97,17 @@ module Schema end end - describe '.compose' do + describe '.concat' do it 'returns a combined registry' do - registry = TypeRegistry.new - registry.register(Shapes::StructureShape.new(id: 'thing2')) - new_registry = TypeRegistry.compose(subject, registry) - expect(new_registry.registry).to include('thing', 'thing2') + registry = TypeRegistry.new('foo' => shape) + another_registry = TypeRegistry.new({ 'bar' => shape, 'baz' => shape }) + new_registry = TypeRegistry.concat(subject, registry, another_registry) + expect(new_registry.registry.keys).to include('foo', 'bar', 'baz') end it 'raises when invalid input is given' do expect do - TypeRegistry.compose(1, 2) + TypeRegistry.concat(subject, 2) end.to raise_error(ArgumentError) end end diff --git a/gems/smithy/lib/smithy/templates/client/schema.erb b/gems/smithy/lib/smithy/templates/client/schema.erb index 5ec5ed694..3cc1fcda1 100644 --- a/gems/smithy/lib/smithy/templates/client/schema.erb +++ b/gems/smithy/lib/smithy/templates/client/schema.erb @@ -47,7 +47,6 @@ module <%= module_name %> <% end -%> end - TYPE_REGISTRY = Smithy::Schema::TypeRegistry.new - TYPE_REGISTRY.register(<%= typed_shapes.map { |s| s.name }.join(', ')%>) + TYPE_REGISTRY = Smithy::Schema::TypeRegistry.new(<%= typed_shapes.join(', ')%>) end end diff --git a/gems/smithy/lib/smithy/views/client/schema.rb b/gems/smithy/lib/smithy/views/client/schema.rb index dadb01740..918fe51ba 100644 --- a/gems/smithy/lib/smithy/views/client/schema.rb +++ b/gems/smithy/lib/smithy/views/client/schema.rb @@ -82,7 +82,7 @@ def shapes_with_members end def typed_shapes - @shapes.select(&:typed) + @shapes.select(&:typed).map { |s| "'#{s.id}' => #{s.name}" } end def operation_shapes From b35e09b6a114b17d60f63c8c2e2cd47abd6077b0 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 21 Apr 2025 09:53:23 -0700 Subject: [PATCH 26/54] Update 1 projection --- projections/shapes/lib/shapes/schema.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/projections/shapes/lib/shapes/schema.rb b/projections/shapes/lib/shapes/schema.rb index 02fd940ef..8f3630b45 100644 --- a/projections/shapes/lib/shapes/schema.rb +++ b/projections/shapes/lib/shapes/schema.rb @@ -75,7 +75,6 @@ module Schema end) end - TYPE_REGISTRY = Smithy::Schema::TypeRegistry.new - TYPE_REGISTRY.register(OperationInputOutput, Structure, Union) + TYPE_REGISTRY = Smithy::Schema::TypeRegistry.new('smithy.ruby.tests#OperationInputOutput' => OperationInputOutput, 'smithy.ruby.tests#Structure' => Structure, 'smithy.ruby.tests#Union' => Union) end end From 66825be7bc592e645e27df6a83a3b6d89db454cb Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 21 Apr 2025 10:02:21 -0700 Subject: [PATCH 27/54] Update weather projection --- projections/weather/lib/weather/schema.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/projections/weather/lib/weather/schema.rb b/projections/weather/lib/weather/schema.rb index 2e4d2bb16..53b72b158 100644 --- a/projections/weather/lib/weather/schema.rb +++ b/projections/weather/lib/weather/schema.rb @@ -84,7 +84,6 @@ module Schema end) end - TYPE_REGISTRY = Smithy::Schema::TypeRegistry.new - TYPE_REGISTRY.register(CityCoordinates, CitySummary, GetCityInput, GetCityOutput, GetCurrentTimeOutput, GetForecastInput, GetForecastOutput, ListCitiesInput, ListCitiesOutput, NoSuchResource) + TYPE_REGISTRY = Smithy::Schema::TypeRegistry.new('example.weather#CityCoordinates' => CityCoordinates, 'example.weather#CitySummary' => CitySummary, 'example.weather#GetCityInput' => GetCityInput, 'example.weather#GetCityOutput' => GetCityOutput, 'example.weather#GetCurrentTimeOutput' => GetCurrentTimeOutput, 'example.weather#GetForecastInput' => GetForecastInput, 'example.weather#GetForecastOutput' => GetForecastOutput, 'example.weather#ListCitiesInput' => ListCitiesInput, 'example.weather#ListCitiesOutput' => ListCitiesOutput, 'example.weather#NoSuchResource' => NoSuchResource) end end From 4efe669e136b80c0d94b4df5e838d2a1a614921c Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 21 Apr 2025 12:25:05 -0700 Subject: [PATCH 28/54] Use SchemaHelper for testing --- .../lib/smithy-schema/document_utils.rb | 7 +- .../spec/smithy-schema/document_spec.rb | 163 ++++++++---------- .../spec/support/schema_helper.rb | 17 +- 3 files changed, 94 insertions(+), 93 deletions(-) diff --git a/gems/smithy-schema/lib/smithy-schema/document_utils.rb b/gems/smithy-schema/lib/smithy-schema/document_utils.rb index 587d7e794..c21a2d1c8 100644 --- a/gems/smithy-schema/lib/smithy-schema/document_utils.rb +++ b/gems/smithy-schema/lib/smithy-schema/document_utils.rb @@ -17,7 +17,9 @@ def format(data) when Time data.to_i # timestamp format is "epoch-seconds" by default when Hash - data.transform_values { |v| format(v) } + data.each_with_object({}) do |(k, v), h| + h[k.to_s] = format(v) + end when Array data.map { |d| format(d) } else @@ -130,7 +132,7 @@ def extract_list(data, shape) def extract_map(data, shape) shape = shape_reference(shape) - data.each.with_object({}) { |(k, v), h| h[k] = extract(v, shape.value) } + data.each.with_object({}) { |(k, v), h| h[k.to_s] = extract(v, shape.value) } end def extract_structure(data, shape, opts) @@ -140,6 +142,7 @@ def extract_structure(data, shape, opts) member_shape = shape.member(k) member_name = resolve_member_name(member_shape, opts) + pp member_name o[member_name] = extract(v, member_shape, opts) end end diff --git a/gems/smithy-schema/spec/smithy-schema/document_spec.rb b/gems/smithy-schema/spec/smithy-schema/document_spec.rb index da3ef8b78..1590deac0 100644 --- a/gems/smithy-schema/spec/smithy-schema/document_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/document_spec.rb @@ -1,83 +1,33 @@ # frozen_string_literal: true require_relative '../spec_helper' +require_relative '../support/schema_helper' module Smithy module Schema describe Document do - let(:simple_runtime) do - Struct.new(:string, keyword_init: true) do - include Smithy::Schema::Structure - end - end + let(:structure_shape) { SchemaHelper.sample_schema.const_get(:Structure) } let(:simple_schema) do shape = Shapes::StructureShape.new(id: 'smithy.ruby.tests#SimpleStructure') string = Shapes::StringShape.new(id: 'smithy.api#String') - shape.add_member(:string, 'stringMember', string) + shape.add_member(:string, 'string', string) shape.type = simple_runtime shape end - let(:runtime) do - Struct.new(:string, :list, :foo_map, :structure, :union, :blob, :timestamp, keyword_init: true) do + let(:simple_runtime) do + Struct.new(:string, keyword_init: true) do include Smithy::Schema::Structure end end - let(:union_runtime) { Class.new(Union) } - let(:union_value_runtime) do - Class.new(union_runtime) do - def to_h - { union_string: super(__getobj__) } - end - - # anonymous class, need a class name to test to_s - def self.name - 'TestUnion::UnionString' - end - end - end - - let(:schema) do - shape = Shapes::StructureShape.new(id: 'smithy.ruby.tests#Structure') - string = Shapes::StringShape.new(id: 'smithy.api#String') - list = Shapes::ListShape.new(id: 'smithy.ruby.tests#List') - list.set_member(Shapes::Prelude::String) - map = Shapes::MapShape.new(id: 'smithy.ruby.tests#Map') - map.set_key(Shapes::Prelude::String) - map.set_value(list) - union = Shapes::UnionShape.new(id: 'smithy.ruby.tests#Union') - union.add_member( - :union_string, - 'unionString', - string, - union_value_runtime, - traits: { 'smithy.api#jsonName' => 'json' } - ) - union.type = union_runtime - shape.add_member(:string, 'stringMember', string, traits: { 'smithy.api#jsonName' => 'json' }) - shape.add_member(:list, 'listMember', list) - shape.add_member(:foo_map, 'mapMember', map) - shape.add_member(:union, 'unionMember', union) - shape.add_member( - :timestamp, - 'timeMember', - Shapes::TimestampShape.new(id: 'smithy.ruby.tests#Timestamp'), - traits: { 'smithy.api#timestampFormat' => 'http-date' } - ) - shape.add_member(:blob, 'blobMember', Shapes::BlobShape.new(id: 'smithy.ruby.tests#Blob')) - shape.add_member(:structure, 'structureMember', shape) - shape.type = runtime - shape - end - context 'untyped document' do - subject { Document.new('foo') } + subject { Document.new(foo: 'bar') } describe '#initialize' do it 'sets data' do - expect(subject.data).to eq('foo') + expect(subject.data).to eq('foo' => 'bar') end it 'sets time data using default format' do @@ -94,11 +44,11 @@ def self.name subject { Document.new({ foo: 'bar' }) } it 'returns member value' do - expect(subject[:foo]).to eq('bar') + expect(subject['foo']).to eq('bar') end it 'returns nil when member key is not applicable' do - expect(subject[:bar]).to be_nil + expect(subject['bar']).to be_nil end end @@ -110,8 +60,8 @@ def self.name describe '#as_typed' do it 'converts document as runtime shape' do - typed_shape = Document.new({ string: 'foo' }).as_typed(simple_schema) - expect(typed_shape).to be_a(simple_runtime) + typed_shape = Document.new({ string: 'foo' }).as_typed(structure_shape) + expect(typed_shape).to be_a(structure_shape.type) expect(typed_shape[:string]).to eq('foo') end @@ -127,47 +77,73 @@ def self.name context 'typed document' do context 'when runtime shape is the input' do let(:typed_shape) do - runtime.new( - string: 'foo', + structure_shape.type.new( + big_decimal: 0, + big_integer: 0, + blob: StringIO.new('foo'), + boolean: true, + byte: 1, + double: 1.1, + float: 1.1, + enum: 'enum', + int_enum: 0, + integer: 1, + long: 1, + short: 1, list: %w[Item1 Item2], - foo_map: { foo: ['Thing1'], bar: ['Thing2'] }, - structure: { list: ['AnotherThing'] }, - union: { union_string: 'hello world' }, + map: { color: 'red' }, + streaming_blob: 'streaming blob', + string: 'foo', + structure_list: [{ integer: 1 }, { integer: 2 }, { integer: 3 }], + structure_map: { 'key' => { map: { 'color' => 'blue' } } }, timestamp: Time.utc(2024, 12, 25), - blob: StringIO.new('foo') + union: { string: 'string' } ) end - subject { Document.new(typed_shape, shape: schema) } + subject { Document.new(typed_shape, shape: structure_shape) } describe '#initialize' do it 'set data' do expect(subject.data).to include( { - 'stringMember' => 'foo', - 'listMember' => %w[Item1 Item2], - 'mapMember' => { foo: ['Thing1'], bar: ['Thing2'] }, - 'structureMember' => { 'listMember' => ['AnotherThing'] }, - 'unionMember' => { 'unionString' => 'hello world' }, - 'timeMember' => 1_735_084_800, - 'blobMember' => 'Zm9v' + 'bigDecimal' => 0, + 'bigInteger' => 0, + 'blob' => 'Zm9v', + 'boolean' => true, + 'byte' => 1, + 'double' => 1.1, + 'float' => 1.1, + 'enum' => 'enum', + 'intEnum' => 0, + 'integer' => 1, + 'long' => 1, + 'short' => 1, + 'list' => %w[Item1 Item2], + 'map' => { 'color' => 'red' }, + 'streamingBlob' => 'c3RyZWFtaW5nIGJsb2I=', + 'string' => 'foo', + 'structureList' => [{ 'integer' => 1 }, { 'integer' => 2 }, { 'integer' => 3 }], + 'structureMap' => { 'key' => { 'map' => { 'color' => 'blue' } } }, + 'timestamp' => 1_735_084_800, + 'union' => { 'string' => 'string' } } ) end it 'set data using jsonName when applicable' do - typed_shape = runtime.new(string: 'foo', union: { union_string: 'bar' }) - doc = Document.new(typed_shape, shape: schema, use_json_name: true) - expect(doc.data).to include({ 'json' => 'foo', 'unionMember' => { 'json' => 'bar' } }) + typed_shape = structure_shape.type.new(string: 'foo', union: { string: 'bar' }) + doc = Document.new(typed_shape, shape: structure_shape, use_json_name: true) + expect(doc.data).to include({ 'jsonName' => 'foo', 'union' => { 'jsonName' => 'bar' } }) end it 'set data using timestampTrait when applicable' do - doc = Document.new(typed_shape, shape: schema, use_timestamp_format: true) - expect(doc.data['timeMember']).to eq('2024-12-25T00:00:00Z') + doc = Document.new(typed_shape, shape: structure_shape, use_timestamp_format: true) + expect(doc.data['timestamp']).to eq('2024-12-25T00:00:00Z') end it 'set discriminator' do - expect(subject.discriminator).to be(schema.id) + expect(subject.discriminator).to be(structure_shape.id) end it 'raises when no schema is given' do @@ -186,16 +162,27 @@ def self.name describe '#as_typed' do it 'converts document as a runtime shape' do - typed_shape = subject.as_typed(schema) + typed_shape = subject.as_typed(structure_shape) expect(typed_shape.to_h).to include( { + big_decimal: 0, + big_integer: 0, + blob: 'foo', + boolean: true, + byte: 1, + double: 1.1, + float: 1.1, + enum: 'enum', + int_enum: 0, + integer: 1, + long: 1, + short: 1, string: 'foo', - list: %w[Item1 Item2], - foo_map: { foo: ['Thing1'], bar: ['Thing2'] }, - structure: { list: ['AnotherThing'] }, - union: { union_string: 'hello world' }, - timestamp: '2024-12-25T00:00:00Z', - blob: 'foo' + streaming_blob: 'streaming blob', + structure_list: [{ integer: 1 }, { integer: 2 }, { integer: 3 }], + structure_map: { 'key' => { map: { 'color' => 'blue' } } }, + union: { string: 'string' }, + timestamp: '2024-12-25T00:00:00Z' } ) end diff --git a/gems/smithy-schema/spec/support/schema_helper.rb b/gems/smithy-schema/spec/support/schema_helper.rb index e9a69609e..c4f925b06 100644 --- a/gems/smithy-schema/spec/support/schema_helper.rb +++ b/gems/smithy-schema/spec/support/schema_helper.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative '../../../smithy/lib/smithy' + module SchemaHelper class << self def sample_shapes @@ -87,18 +89,27 @@ def sample_shapes 'target' => 'smithy.ruby.tests#StreamingBlob', 'traits' => { 'smithy.api#default' => 'streamingBlob' } }, - 'string' => { 'target' => 'smithy.api#String' }, + 'string' => { + 'target' => 'smithy.api#String', + 'traits' => { 'smithy.api#jsonName' => 'jsonName' } + }, 'structure' => { 'target' => 'smithy.ruby.tests#Structure' }, 'structureList' => { 'target' => 'smithy.ruby.tests#StructureList' }, 'structureMap' => { 'target' => 'smithy.ruby.tests#StructureMap' }, - 'timestamp' => { 'target' => 'smithy.api#Timestamp' }, + 'timestamp' => { + 'target' => 'smithy.api#Timestamp', + 'traits' => { 'smithy.api#timestampFormat' => 'http-date' } + }, 'union' => { 'target' => 'smithy.ruby.tests#Union' } } }, 'smithy.ruby.tests#Union' => { 'type' => 'union', 'members' => { - 'string' => { 'target' => 'smithy.api#String' }, + 'string' => { + 'target' => 'smithy.api#String', + 'traits' => { 'smithy.api#jsonName' => 'jsonName' } + }, 'structure' => { 'target' => 'smithy.ruby.tests#Structure' }, 'unit' => { 'target' => 'smithy.api#Unit' } } From 73e0e83d36b06205bb02de86ebe1978a307decaa Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 21 Apr 2025 12:37:33 -0700 Subject: [PATCH 29/54] Update TypeRegistry to use SchemaHelper for testing --- .../spec/smithy-schema/type_registry_spec.rb | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/gems/smithy-schema/spec/smithy-schema/type_registry_spec.rb b/gems/smithy-schema/spec/smithy-schema/type_registry_spec.rb index 36659ba12..37b979384 100644 --- a/gems/smithy-schema/spec/smithy-schema/type_registry_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/type_registry_spec.rb @@ -1,32 +1,22 @@ # frozen_string_literal: true require_relative '../spec_helper' +require_relative '../support/schema_helper' module Smithy module Schema describe TypeRegistry do + subject { TypeRegistry.new({ 'thing' => shape }) } - let(:runtime_shape) do - Struct.new(:string, keyword_init: true) do - include Smithy::Schema::Structure - end - end + let(:shape) { SchemaHelper.sample_schema.const_get(:Structure) } - let(:fake_shape) do + let(:fake_type) do Struct.new(:foo, keyword_init: true) do include Smithy::Schema::Structure end end - let(:shape) do - shape = Shapes::StructureShape.new(id: 'thing') - string = Shapes::StringShape.new(id: 'smithy.api#String') - shape.add_member(:string, 'stringMember', string) - shape.type = runtime_shape - shape - end - describe '#initialize' do subject { TypeRegistry.new } @@ -79,21 +69,21 @@ module Schema describe '#shape_by_type?' do it 'returns true if registered' do - expect(subject.shape_by_type?(runtime_shape)).to be true + expect(subject.shape_by_type?(shape.type)).to be true end it 'returns false if not registered' do - expect(subject.shape_by_type?(fake_shape)).to be false + expect(subject.shape_by_type?(fake_type)).to be false end end describe '#shape_by_type' do it 'returns shape' do - expect(subject.shape_by_type(runtime_shape)).to be(shape) + expect(subject.shape_by_type(shape.type)).to be(shape) end it 'returns nil if shape is not found' do - expect(subject.shape_by_type(fake_shape)).to be_nil + expect(subject.shape_by_type(fake_type)).to be_nil end end From 60ef29c0c01aa58e24883afe7c3055aef7b50271 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 21 Apr 2025 13:51:53 -0700 Subject: [PATCH 30/54] Add TypeRegistry documentation --- .../lib/smithy-schema/document.rb | 10 ++--- .../lib/smithy-schema/type_registry.rb | 42 ++++++++++++++++--- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/gems/smithy-schema/lib/smithy-schema/document.rb b/gems/smithy-schema/lib/smithy-schema/document.rb index 404fc18a9..81b01c035 100644 --- a/gems/smithy-schema/lib/smithy-schema/document.rb +++ b/gems/smithy-schema/lib/smithy-schema/document.rb @@ -32,7 +32,7 @@ def initialize(data, options = {}) # @return [String] discriminator attr_reader :discriminator - # @param [Object] key + # @param [String] key # @return [Object] def [](key) return unless @data.is_a?(Hash) && @data.key?(key) @@ -40,8 +40,8 @@ def [](key) @data[key] end - # @param [Shapes::Shape] shape - # @return [Shapes::Structure] typed shape + # @param [Shapes::Structure] shape + # @return [Object] typed shape def as_typed(shape) error_message = 'Invalid shape or document data' raise ArgumentError, error_message unless valid_shape?(shape) && @data.is_a?(Hash) @@ -82,15 +82,11 @@ def set_data(data, opts) end opts = opts.except(:shape) - # case 1 - extract data from runtime shape, shape is required to know to properly extract DocumentUtils.extract(data, shape, opts) - else if discriminator?(data) - # case 2 - extract typed data from parsed JSON data.except('__type') else - # case 3 - untyped data, we will need consolidate timestamps and such DocumentUtils.format(data) end end diff --git a/gems/smithy-schema/lib/smithy-schema/type_registry.rb b/gems/smithy-schema/lib/smithy-schema/type_registry.rb index 852e7c35b..bfb5c4609 100644 --- a/gems/smithy-schema/lib/smithy-schema/type_registry.rb +++ b/gems/smithy-schema/lib/smithy-schema/type_registry.rb @@ -2,10 +2,40 @@ module Smithy module Schema - # A registry that contains a map of Smithy shape ID to its shape. - # Also includes a way to find shape based on its type representation. + # A registry that contains a map of Smithy shape ID to the shape representation. + # + # This registry has the following functionalities: + # + # * Access shape by shape ID + # * Access shape by its type + # * Register shape to the Registry + # * Supports enumeration of registered shapes + # + # You could also combine multiple registries into one registry. + # + # @example Creating a new Registry + # # accepts a map of id/shapes + # registry = TypeRegistry.new( + # "someId" => StructureShape, + # "anotherId" => AnotherStructureShape + # ) + # + # @example Shape Lookup + # # Find shape by its shape id + # registry["someId"] + # # => # + # + # # Find shape by its type + # registry.shape_by_type(ExampleService::Types::Structure) + # # => # + # + # @example Combining multiple registries + # TypeRegistry.concat(registry1, registry2) + # # => # class TypeRegistry include Enumerable + + # @param [Hash] registry def initialize(registry = {}) @registry = registry @shapes_by_type = register_shape_types(registry.values) @@ -16,7 +46,7 @@ def initialize(registry = {}) attr_accessor :registry # @api private - # @return [Hash] + # @return [Hash] attr_reader :shapes_by_type # @return [Hash] @@ -30,6 +60,8 @@ def [](id) @registry[id] end + # @param [String] id + # @param [Shapes::Structure] shape def []=(id, shape) msg = 'Expected a shape with members and type' raise ArgumentError, msg unless shape.is_a?(Shapes::Structure) && shape.type @@ -47,14 +79,14 @@ def key?(id) # Returns true if the registry contains a shape associated # with the given type. - # @param [Struct] type + # @param [Class] type # @return [Boolean] def shape_by_type?(type) @shapes_by_type.key?(type) end # Returns the shape registered for the given type. - # @param [Struct] type + # @param [Class] type # @return [Shapes::Structure, nil] def shape_by_type(type) @shapes_by_type[type] From b67cf5435f0771b3985f5968bf776ebb579c156a Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 28 Apr 2025 08:42:29 -0700 Subject: [PATCH 31/54] Update example --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9818749e8..6f59c7efa 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,8 @@ irb -I projections/weather/lib -I gems/smithy-client/lib -I gems/smithy-schema/l Create a Weather client: ``` -protocol = Smithy::Client::Protocols::RPCv2.new -client = Weather::Client.new(endpoint: 'https://example.com', protocol: protocol) +protocol = Smithy::Client::RPCv2CBOR::Protocol.new +client = Weather::Client.new(stub_responses: true, protocol: protocol) client.get_city(city_id: '1') client.get_current_time ``` From 381602b64dc73718209be51617d93011caf42cf5 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 28 Apr 2025 08:58:58 -0700 Subject: [PATCH 32/54] Remove reference to type registry from client --- gems/smithy-client/lib/smithy-client/base.rb | 10 ---------- gems/smithy-client/spec/smithy-client/base_spec.rb | 14 -------------- gems/smithy/lib/smithy/templates/client/client.erb | 1 - 3 files changed, 25 deletions(-) diff --git a/gems/smithy-client/lib/smithy-client/base.rb b/gems/smithy-client/lib/smithy-client/base.rb index 0c05c5824..8f8e07d56 100644 --- a/gems/smithy-client/lib/smithy-client/base.rb +++ b/gems/smithy-client/lib/smithy-client/base.rb @@ -158,16 +158,6 @@ def service # @param [ServiceShape] service attr_writer :service - # @return [Schema::TypeRegistry] - def type_registry - @type_registry ||= Schema::TypeRegistry.new - end - - # @param [TypeRegistry] registry - def set_type_registry=(registry) - @type_registry = registry - end - # @option options [ServiceShape] :service (ServiceShape.new) # @option options [Array] :plugins ([]) A list of plugins to # add to the client class created. diff --git a/gems/smithy-client/spec/smithy-client/base_spec.rb b/gems/smithy-client/spec/smithy-client/base_spec.rb index 1954f2738..d4a1c436a 100644 --- a/gems/smithy-client/spec/smithy-client/base_spec.rb +++ b/gems/smithy-client/spec/smithy-client/base_spec.rb @@ -218,20 +218,6 @@ module Client end end - describe '.type_registry' do - it 'defaults to a TypeRegistry' do - expect(client_class.type_registry).to be_kind_of(Schema::TypeRegistry) - end - end - - describe '.set_type_registry' do - it 'can be set' do - registry = Schema::TypeRegistry.new - client_class.set_type_registry = registry - expect(client_class.type_registry).to be(registry) - end - end - describe '.define' do it 'creates a new client class' do client_class = Base.define diff --git a/gems/smithy/lib/smithy/templates/client/client.erb b/gems/smithy/lib/smithy/templates/client/client.erb index e8061f911..2fdd73b9b 100644 --- a/gems/smithy/lib/smithy/templates/client/client.erb +++ b/gems/smithy/lib/smithy/templates/client/client.erb @@ -13,7 +13,6 @@ module <%= module_name %> include Smithy::Client::Stubs self.service = Schema::SERVICE - self.set_type_registry = Schema::TYPE_REGISTRY <% add_plugins.each do |plugin_class| -%> add_plugin(<%= plugin_class %>) From 6d41f645fedaadd9e20e3eaa793c65ed5d47c38d Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 28 Apr 2025 09:02:03 -0700 Subject: [PATCH 33/54] Update projections --- projections/shapes/lib/shapes/client.rb | 1 - .../spec/shapes/endpoint_provider_spec.rb | 2 +- projections/weather/lib/weather/client.rb | 1 - projections/weather/lib/weather/schema.rb | 64 ++++++++----------- .../spec/weather/endpoint_provider_spec.rb | 6 +- 5 files changed, 32 insertions(+), 42 deletions(-) diff --git a/projections/shapes/lib/shapes/client.rb b/projections/shapes/lib/shapes/client.rb index 075a900fe..df5f12708 100644 --- a/projections/shapes/lib/shapes/client.rb +++ b/projections/shapes/lib/shapes/client.rb @@ -29,7 +29,6 @@ class Client < Smithy::Client::Base include Smithy::Client::Stubs self.service = Schema::SERVICE - self.set_type_registry = Schema::TYPE_REGISTRY add_plugin(ShapeService::Plugins::Auth) add_plugin(ShapeService::Plugins::Endpoint) diff --git a/projections/shapes/spec/shapes/endpoint_provider_spec.rb b/projections/shapes/spec/shapes/endpoint_provider_spec.rb index 867d0d298..ef6f43250 100644 --- a/projections/shapes/spec/shapes/endpoint_provider_spec.rb +++ b/projections/shapes/spec/shapes/endpoint_provider_spec.rb @@ -14,7 +14,7 @@ module ShapeService end it 'produces the expected output from the EndpointProvider' do - params = EndpointParameters.new(**{endpoint: "https://example.com"}) + params = EndpointParameters.new(**{:endpoint=>"https://example.com"}) endpoint = subject.resolve(params) expect(endpoint.uri).to eq(expected['endpoint']['url']) expect(endpoint.headers).to eq(expected['endpoint']['headers'] || {}) diff --git a/projections/weather/lib/weather/client.rb b/projections/weather/lib/weather/client.rb index d9868b35a..c70176b9c 100644 --- a/projections/weather/lib/weather/client.rb +++ b/projections/weather/lib/weather/client.rb @@ -29,7 +29,6 @@ class Client < Smithy::Client::Base include Smithy::Client::Stubs self.service = Schema::SERVICE - self.set_type_registry = Schema::TYPE_REGISTRY add_plugin(Weather::Plugins::Auth) add_plugin(Weather::Plugins::Endpoint) diff --git a/projections/weather/lib/weather/schema.rb b/projections/weather/lib/weather/schema.rb index 6c307e83f..2c9a87e58 100644 --- a/projections/weather/lib/weather/schema.rb +++ b/projections/weather/lib/weather/schema.rb @@ -8,66 +8,56 @@ module Schema include Smithy::Schema::Shapes CityCoordinates = StructureShape.new(id: 'example.weather#CityCoordinates') - CityId = StringShape.new(id: 'example.weather#CityId', traits: {"smithy.api#pattern" => "^[A-Za-z0-9 ]+$"}) + CityId = StringShape.new(id: 'example.weather#CityId', traits: {"smithy.api#pattern"=>"^[A-Za-z0-9 ]+$"}) CitySummaries = ListShape.new(id: 'example.weather#CitySummaries') - CitySummary = StructureShape.new(id: 'example.weather#CitySummary', traits: {"smithy.api#references" => [{"resource" => "example.weather#City"}]}) - GetCityInput = StructureShape.new(id: 'example.weather#GetCityInput', traits: {"smithy.api#input" => {}}) - GetCityOutput = StructureShape.new(id: 'example.weather#GetCityOutput', traits: {"smithy.api#output" => {}}) - GetCurrentTimeOutput = StructureShape.new(id: 'example.weather#GetCurrentTimeOutput', traits: {"smithy.api#output" => {}}) - GetForecastInput = StructureShape.new(id: 'example.weather#GetForecastInput', traits: {"smithy.api#input" => {}}) - GetForecastOutput = StructureShape.new(id: 'example.weather#GetForecastOutput', traits: {"smithy.api#output" => {}}) - ListCitiesInput = StructureShape.new(id: 'example.weather#ListCitiesInput', traits: {"smithy.api#input" => {}}) - ListCitiesOutput = StructureShape.new(id: 'example.weather#ListCitiesOutput', traits: {"smithy.api#output" => {}}) - NoSuchResource = StructureShape.new(id: 'example.weather#NoSuchResource', traits: {"smithy.api#error" => "client"}) + CitySummary = StructureShape.new(id: 'example.weather#CitySummary', traits: {"smithy.api#references"=>[{"resource"=>"example.weather#City"}]}) + GetCityInput = StructureShape.new(id: 'example.weather#GetCityInput', traits: {"smithy.api#input"=>{}}) + GetCityOutput = StructureShape.new(id: 'example.weather#GetCityOutput', traits: {"smithy.api#output"=>{}}) + GetCurrentTimeOutput = StructureShape.new(id: 'example.weather#GetCurrentTimeOutput', traits: {"smithy.api#output"=>{}}) + GetForecastInput = StructureShape.new(id: 'example.weather#GetForecastInput', traits: {"smithy.api#input"=>{}}) + GetForecastOutput = StructureShape.new(id: 'example.weather#GetForecastOutput', traits: {"smithy.api#output"=>{}}) + ListCitiesInput = StructureShape.new(id: 'example.weather#ListCitiesInput', traits: {"smithy.api#input"=>{}}) + ListCitiesOutput = StructureShape.new(id: 'example.weather#ListCitiesOutput', traits: {"smithy.api#output"=>{}}) + NoSuchResource = StructureShape.new(id: 'example.weather#NoSuchResource', traits: {"smithy.api#error"=>"client"}) - CityCoordinates.add_member(:latitude, 'latitude', Prelude::Float, traits: {"smithy.api#required" => {}}) - CityCoordinates.add_member(:longitude, 'longitude', Prelude::Float, traits: {"smithy.api#required" => {}}) + CityCoordinates.add_member(:latitude, 'latitude', Prelude::Float, traits: {"smithy.api#required"=>{}}) + CityCoordinates.add_member(:longitude, 'longitude', Prelude::Float, traits: {"smithy.api#required"=>{}}) CityCoordinates.type = Types::CityCoordinates - CitySummaries.set_member(CitySummary) - - CitySummary.add_member(:city_id, 'cityId', CityId, traits: {"smithy.api#required" => {}}) - CitySummary.add_member(:name, 'name', Prelude::String, traits: {"smithy.api#required" => {}}) + CitySummary.add_member(:city_id, 'cityId', CityId, traits: {"smithy.api#required"=>{}}) + CitySummary.add_member(:name, 'name', Prelude::String, traits: {"smithy.api#required"=>{}}) CitySummary.type = Types::CitySummary - - GetCityInput.add_member(:city_id, 'cityId', CityId, traits: {"smithy.api#required" => {}}) + GetCityInput.add_member(:city_id, 'cityId', CityId, traits: {"smithy.api#required"=>{}}) GetCityInput.type = Types::GetCityInput - - GetCityOutput.add_member(:name, 'name', Prelude::String, traits: {"smithy.api#notProperty" => {}, "smithy.api#required" => {}}) - GetCityOutput.add_member(:coordinates, 'coordinates', CityCoordinates, traits: {"smithy.api#required" => {}}) + GetCityOutput.add_member(:name, 'name', Prelude::String, traits: {"smithy.api#notProperty"=>{}, "smithy.api#required"=>{}}) + GetCityOutput.add_member(:coordinates, 'coordinates', CityCoordinates, traits: {"smithy.api#required"=>{}}) GetCityOutput.type = Types::GetCityOutput - - GetCurrentTimeOutput.add_member(:time, 'time', Prelude::Timestamp, traits: {"smithy.api#required" => {}}) + GetCurrentTimeOutput.add_member(:time, 'time', Prelude::Timestamp, traits: {"smithy.api#required"=>{}}) GetCurrentTimeOutput.type = Types::GetCurrentTimeOutput - - GetForecastInput.add_member(:city_id, 'cityId', CityId, traits: {"smithy.api#required" => {}}) + GetForecastInput.add_member(:city_id, 'cityId', CityId, traits: {"smithy.api#required"=>{}}) GetForecastInput.type = Types::GetForecastInput - GetForecastOutput.add_member(:chance_of_rain, 'chanceOfRain', Prelude::Float) GetForecastOutput.type = Types::GetForecastOutput - ListCitiesInput.add_member(:next_token, 'nextToken', Prelude::String) ListCitiesInput.add_member(:page_size, 'pageSize', Prelude::Integer) ListCitiesInput.type = Types::ListCitiesInput - ListCitiesOutput.add_member(:next_token, 'nextToken', Prelude::String) - ListCitiesOutput.add_member(:items, 'items', CitySummaries, traits: {"smithy.api#required" => {}}) + ListCitiesOutput.add_member(:items, 'items', CitySummaries, traits: {"smithy.api#required"=>{}}) ListCitiesOutput.type = Types::ListCitiesOutput - - NoSuchResource.add_member(:resource_type, 'resourceType', Prelude::String, traits: {"smithy.api#required" => {}}) + NoSuchResource.add_member(:resource_type, 'resourceType', Prelude::String, traits: {"smithy.api#required"=>{}}) NoSuchResource.type = Types::NoSuchResource SERVICE = ServiceShape.new do |service| service.id = "example.weather#Weather" service.name = "Weather" service.version = "2006-03-01" - service.traits = {"smithy.api#paginated" => {"inputToken" => "nextToken", "outputToken" => "nextToken", "pageSize" => "pageSize"}} + service.traits = {"smithy.api#paginated"=>{"inputToken"=>"nextToken", "outputToken"=>"nextToken", "pageSize"=>"pageSize"}} service.add_operation(:get_city, OperationShape.new do |operation| operation.id = "example.weather#GetCity" operation.name = "GetCity" operation.input = GetCityInput operation.output = GetCityOutput - operation.traits = {"smithy.api#readonly" => {}} + operation.traits = {"smithy.api#readonly"=>{}} operation.errors << NoSuchResource end) service.add_operation(:get_current_time, OperationShape.new do |operation| @@ -75,23 +65,25 @@ module Schema operation.name = "GetCurrentTime" operation.input = Prelude::Unit operation.output = GetCurrentTimeOutput - operation.traits = {"smithy.api#readonly" => {}} + operation.traits = {"smithy.api#readonly"=>{}} end) service.add_operation(:get_forecast, OperationShape.new do |operation| operation.id = "example.weather#GetForecast" operation.name = "GetForecast" operation.input = GetForecastInput operation.output = GetForecastOutput - operation.traits = {"smithy.api#readonly" => {}} + operation.traits = {"smithy.api#readonly"=>{}} end) service.add_operation(:list_cities, OperationShape.new do |operation| operation.id = "example.weather#ListCities" operation.name = "ListCities" operation.input = ListCitiesInput operation.output = ListCitiesOutput - operation.traits = {"smithy.api#paginated" => {"items" => "items"}, "smithy.api#readonly" => {}} + operation.traits = {"smithy.api#paginated"=>{"items"=>"items"}, "smithy.api#readonly"=>{}} operation[:paginator] = Paginators::ListCities.new end) end + + TYPE_REGISTRY = Smithy::Schema::TypeRegistry.new('example.weather#CityCoordinates' => CityCoordinates, 'example.weather#CitySummary' => CitySummary, 'example.weather#GetCityInput' => GetCityInput, 'example.weather#GetCityOutput' => GetCityOutput, 'example.weather#GetCurrentTimeOutput' => GetCurrentTimeOutput, 'example.weather#GetForecastInput' => GetForecastInput, 'example.weather#GetForecastOutput' => GetForecastOutput, 'example.weather#ListCitiesInput' => ListCitiesInput, 'example.weather#ListCitiesOutput' => ListCitiesOutput, 'example.weather#NoSuchResource' => NoSuchResource) end end diff --git a/projections/weather/spec/weather/endpoint_provider_spec.rb b/projections/weather/spec/weather/endpoint_provider_spec.rb index 226652f4c..5b7901ebd 100644 --- a/projections/weather/spec/weather/endpoint_provider_spec.rb +++ b/projections/weather/spec/weather/endpoint_provider_spec.rb @@ -10,11 +10,11 @@ module Weather context "Endpoint set" do let(:expected) do - {"endpoint" => {"url" => "https://example.com"}} + {"endpoint"=>{"url"=>"https://example.com"}} end it 'produces the expected output from the EndpointProvider' do - params = EndpointParameters.new(**{endpoint: "https://example.com"}) + params = EndpointParameters.new(**{:endpoint=>"https://example.com"}) endpoint = subject.resolve(params) expect(endpoint.uri).to eq(expected['endpoint']['url']) expect(endpoint.headers).to eq(expected['endpoint']['headers'] || {}) @@ -25,7 +25,7 @@ module Weather context "Endpoint not set" do let(:expected) do - {"error" => "Endpoint is not set - you must configure an endpoint."} + {"error"=>"Endpoint is not set - you must configure an endpoint."} end it 'produces the expected output from the EndpointProvider' do From 83fa2bbb77d7c435316784e2d09c245b6fe52375 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 28 Apr 2025 09:04:17 -0700 Subject: [PATCH 34/54] Update projections --- projections/shapes/lib/shapes/schema.rb | 60 +++++++++---------- .../spec/shapes/endpoint_provider_spec.rb | 6 +- projections/weather/lib/weather/schema.rb | 52 ++++++++-------- .../spec/weather/endpoint_provider_spec.rb | 6 +- 4 files changed, 62 insertions(+), 62 deletions(-) diff --git a/projections/shapes/lib/shapes/schema.rb b/projections/shapes/lib/shapes/schema.rb index 8f3630b45..21386cb84 100644 --- a/projections/shapes/lib/shapes/schema.rb +++ b/projections/shapes/lib/shapes/schema.rb @@ -7,32 +7,32 @@ module ShapeService module Schema include Smithy::Schema::Shapes - BigDecimal = BigDecimalShape.new(id: 'smithy.ruby.tests#BigDecimal', traits: {"smithy.ruby.tests#shape"=>{}}) - BigInteger = IntegerShape.new(id: 'smithy.ruby.tests#BigInteger', traits: {"smithy.ruby.tests#shape"=>{}}) - Blob = BlobShape.new(id: 'smithy.ruby.tests#Blob', traits: {"smithy.ruby.tests#shape"=>{}}) - Boolean = BooleanShape.new(id: 'smithy.ruby.tests#Boolean', traits: {"smithy.ruby.tests#shape"=>{}}) - Byte = IntegerShape.new(id: 'smithy.ruby.tests#Byte', traits: {"smithy.ruby.tests#shape"=>{}}) - Document = DocumentShape.new(id: 'smithy.ruby.tests#Document', traits: {"smithy.ruby.tests#shape"=>{}}) - Double = FloatShape.new(id: 'smithy.ruby.tests#Double', traits: {"smithy.ruby.tests#shape"=>{}}) - Enum = EnumShape.new(id: 'smithy.ruby.tests#Enum', traits: {"smithy.ruby.tests#shape"=>{}}) - Float = FloatShape.new(id: 'smithy.ruby.tests#Float', traits: {"smithy.ruby.tests#shape"=>{}}) - IntEnum = IntEnumShape.new(id: 'smithy.ruby.tests#IntEnum', traits: {"smithy.ruby.tests#shape"=>{}}) - Integer = IntegerShape.new(id: 'smithy.ruby.tests#Integer', traits: {"smithy.ruby.tests#shape"=>{}}) - List = ListShape.new(id: 'smithy.ruby.tests#List', traits: {"smithy.ruby.tests#shape"=>{}}) - Long = IntegerShape.new(id: 'smithy.ruby.tests#Long', traits: {"smithy.ruby.tests#shape"=>{}}) - Map = MapShape.new(id: 'smithy.ruby.tests#Map', traits: {"smithy.ruby.tests#shape"=>{}}) + BigDecimal = BigDecimalShape.new(id: 'smithy.ruby.tests#BigDecimal', traits: {"smithy.ruby.tests#shape" => {}}) + BigInteger = IntegerShape.new(id: 'smithy.ruby.tests#BigInteger', traits: {"smithy.ruby.tests#shape" => {}}) + Blob = BlobShape.new(id: 'smithy.ruby.tests#Blob', traits: {"smithy.ruby.tests#shape" => {}}) + Boolean = BooleanShape.new(id: 'smithy.ruby.tests#Boolean', traits: {"smithy.ruby.tests#shape" => {}}) + Byte = IntegerShape.new(id: 'smithy.ruby.tests#Byte', traits: {"smithy.ruby.tests#shape" => {}}) + Document = DocumentShape.new(id: 'smithy.ruby.tests#Document', traits: {"smithy.ruby.tests#shape" => {}}) + Double = FloatShape.new(id: 'smithy.ruby.tests#Double', traits: {"smithy.ruby.tests#shape" => {}}) + Enum = EnumShape.new(id: 'smithy.ruby.tests#Enum', traits: {"smithy.ruby.tests#shape" => {}}) + Float = FloatShape.new(id: 'smithy.ruby.tests#Float', traits: {"smithy.ruby.tests#shape" => {}}) + IntEnum = IntEnumShape.new(id: 'smithy.ruby.tests#IntEnum', traits: {"smithy.ruby.tests#shape" => {}}) + Integer = IntegerShape.new(id: 'smithy.ruby.tests#Integer', traits: {"smithy.ruby.tests#shape" => {}}) + List = ListShape.new(id: 'smithy.ruby.tests#List', traits: {"smithy.ruby.tests#shape" => {}}) + Long = IntegerShape.new(id: 'smithy.ruby.tests#Long', traits: {"smithy.ruby.tests#shape" => {}}) + Map = MapShape.new(id: 'smithy.ruby.tests#Map', traits: {"smithy.ruby.tests#shape" => {}}) OperationInputOutput = StructureShape.new(id: 'smithy.ruby.tests#OperationInputOutput') - Short = IntegerShape.new(id: 'smithy.ruby.tests#Short', traits: {"smithy.ruby.tests#shape"=>{}}) - String = StringShape.new(id: 'smithy.ruby.tests#String', traits: {"smithy.ruby.tests#shape"=>{}}) - Structure = StructureShape.new(id: 'smithy.ruby.tests#Structure', traits: {"smithy.ruby.tests#shape"=>{}}) - Timestamp = TimestampShape.new(id: 'smithy.ruby.tests#Timestamp', traits: {"smithy.ruby.tests#shape"=>{}}) - Union = UnionShape.new(id: 'smithy.ruby.tests#Union', traits: {"smithy.ruby.tests#shape"=>{}}) + Short = IntegerShape.new(id: 'smithy.ruby.tests#Short', traits: {"smithy.ruby.tests#shape" => {}}) + String = StringShape.new(id: 'smithy.ruby.tests#String', traits: {"smithy.ruby.tests#shape" => {}}) + Structure = StructureShape.new(id: 'smithy.ruby.tests#Structure', traits: {"smithy.ruby.tests#shape" => {}}) + Timestamp = TimestampShape.new(id: 'smithy.ruby.tests#Timestamp', traits: {"smithy.ruby.tests#shape" => {}}) + Union = UnionShape.new(id: 'smithy.ruby.tests#Union', traits: {"smithy.ruby.tests#shape" => {}}) - Enum.add_member(:foo, 'FOO', Prelude::Unit, traits: {"smithy.api#enumValue"=>"bar"}) - IntEnum.add_member(:baz, 'BAZ', Prelude::Unit, traits: {"smithy.api#enumValue"=>1}) - List.set_member(String, traits: {"smithy.ruby.tests#shape"=>{}}) - Map.set_key(String, traits: {"smithy.ruby.tests#shape"=>{}}) - Map.set_value(String, traits: {"smithy.ruby.tests#shape"=>{}}) + Enum.add_member(:foo, 'FOO', Prelude::Unit, traits: {"smithy.api#enumValue" => "bar"}) + IntEnum.add_member(:baz, 'BAZ', Prelude::Unit, traits: {"smithy.api#enumValue" => 1}) + List.set_member(String, traits: {"smithy.ruby.tests#shape" => {}}) + Map.set_key(String, traits: {"smithy.ruby.tests#shape" => {}}) + Map.set_value(String, traits: {"smithy.ruby.tests#shape" => {}}) OperationInputOutput.add_member(:blob, 'blob', Blob) OperationInputOutput.add_member(:boolean, 'boolean', Boolean) OperationInputOutput.add_member(:string, 'string', String) @@ -53,11 +53,11 @@ module Schema OperationInputOutput.add_member(:structure, 'structure', Structure) OperationInputOutput.add_member(:union, 'union', Union) OperationInputOutput.type = Types::OperationInputOutput - Structure.add_member(:member, 'member', String, traits: {"smithy.ruby.tests#shape"=>{}}) + Structure.add_member(:member, 'member', String, traits: {"smithy.ruby.tests#shape" => {}}) Structure.type = Types::Structure - Union.add_member(:string, 'string', String, Types::Union::String, traits: {"smithy.ruby.tests#shape"=>{}}) - Union.add_member(:structure, 'structure', Structure, Types::Union::Structure, traits: {"smithy.ruby.tests#shape"=>{}}) - Union.add_member(:unit, 'unit', Prelude::Unit, Types::Union::Unit, traits: {"smithy.ruby.tests#shape"=>{}}) + Union.add_member(:string, 'string', String, Types::Union::String, traits: {"smithy.ruby.tests#shape" => {}}) + Union.add_member(:structure, 'structure', Structure, Types::Union::Structure, traits: {"smithy.ruby.tests#shape" => {}}) + Union.add_member(:unit, 'unit', Prelude::Unit, Types::Union::Unit, traits: {"smithy.ruby.tests#shape" => {}}) Union.add_member(:unknown, 'unknown', Prelude::Unit, Types::Union::Unknown) Union.type = Types::Union @@ -65,13 +65,13 @@ module Schema service.id = "smithy.ruby.tests#ShapeService" service.name = "ShapeService" service.version = "2018-10-31" - service.traits = {"smithy.ruby.tests#shape"=>{}} + service.traits = {"smithy.ruby.tests#shape" => {}} service.add_operation(:operation, OperationShape.new do |operation| operation.id = "smithy.ruby.tests#Operation" operation.name = "Operation" operation.input = OperationInputOutput operation.output = OperationInputOutput - operation.traits = {"smithy.ruby.tests#shape"=>{}} + operation.traits = {"smithy.ruby.tests#shape" => {}} end) end diff --git a/projections/shapes/spec/shapes/endpoint_provider_spec.rb b/projections/shapes/spec/shapes/endpoint_provider_spec.rb index ef6f43250..4a984e3c4 100644 --- a/projections/shapes/spec/shapes/endpoint_provider_spec.rb +++ b/projections/shapes/spec/shapes/endpoint_provider_spec.rb @@ -10,11 +10,11 @@ module ShapeService context "Endpoint set" do let(:expected) do - {"endpoint"=>{"url"=>"https://example.com"}} + {"endpoint" => {"url" => "https://example.com"}} end it 'produces the expected output from the EndpointProvider' do - params = EndpointParameters.new(**{:endpoint=>"https://example.com"}) + params = EndpointParameters.new(**{endpoint: "https://example.com"}) endpoint = subject.resolve(params) expect(endpoint.uri).to eq(expected['endpoint']['url']) expect(endpoint.headers).to eq(expected['endpoint']['headers'] || {}) @@ -25,7 +25,7 @@ module ShapeService context "Endpoint not set" do let(:expected) do - {"error"=>"Endpoint is not set - you must configure an endpoint."} + {"error" => "Endpoint is not set - you must configure an endpoint."} end it 'produces the expected output from the EndpointProvider' do diff --git a/projections/weather/lib/weather/schema.rb b/projections/weather/lib/weather/schema.rb index 2c9a87e58..cca646bd1 100644 --- a/projections/weather/lib/weather/schema.rb +++ b/projections/weather/lib/weather/schema.rb @@ -8,33 +8,33 @@ module Schema include Smithy::Schema::Shapes CityCoordinates = StructureShape.new(id: 'example.weather#CityCoordinates') - CityId = StringShape.new(id: 'example.weather#CityId', traits: {"smithy.api#pattern"=>"^[A-Za-z0-9 ]+$"}) + CityId = StringShape.new(id: 'example.weather#CityId', traits: {"smithy.api#pattern" => "^[A-Za-z0-9 ]+$"}) CitySummaries = ListShape.new(id: 'example.weather#CitySummaries') - CitySummary = StructureShape.new(id: 'example.weather#CitySummary', traits: {"smithy.api#references"=>[{"resource"=>"example.weather#City"}]}) - GetCityInput = StructureShape.new(id: 'example.weather#GetCityInput', traits: {"smithy.api#input"=>{}}) - GetCityOutput = StructureShape.new(id: 'example.weather#GetCityOutput', traits: {"smithy.api#output"=>{}}) - GetCurrentTimeOutput = StructureShape.new(id: 'example.weather#GetCurrentTimeOutput', traits: {"smithy.api#output"=>{}}) - GetForecastInput = StructureShape.new(id: 'example.weather#GetForecastInput', traits: {"smithy.api#input"=>{}}) - GetForecastOutput = StructureShape.new(id: 'example.weather#GetForecastOutput', traits: {"smithy.api#output"=>{}}) - ListCitiesInput = StructureShape.new(id: 'example.weather#ListCitiesInput', traits: {"smithy.api#input"=>{}}) - ListCitiesOutput = StructureShape.new(id: 'example.weather#ListCitiesOutput', traits: {"smithy.api#output"=>{}}) - NoSuchResource = StructureShape.new(id: 'example.weather#NoSuchResource', traits: {"smithy.api#error"=>"client"}) + CitySummary = StructureShape.new(id: 'example.weather#CitySummary', traits: {"smithy.api#references" => [{"resource" => "example.weather#City"}]}) + GetCityInput = StructureShape.new(id: 'example.weather#GetCityInput', traits: {"smithy.api#input" => {}}) + GetCityOutput = StructureShape.new(id: 'example.weather#GetCityOutput', traits: {"smithy.api#output" => {}}) + GetCurrentTimeOutput = StructureShape.new(id: 'example.weather#GetCurrentTimeOutput', traits: {"smithy.api#output" => {}}) + GetForecastInput = StructureShape.new(id: 'example.weather#GetForecastInput', traits: {"smithy.api#input" => {}}) + GetForecastOutput = StructureShape.new(id: 'example.weather#GetForecastOutput', traits: {"smithy.api#output" => {}}) + ListCitiesInput = StructureShape.new(id: 'example.weather#ListCitiesInput', traits: {"smithy.api#input" => {}}) + ListCitiesOutput = StructureShape.new(id: 'example.weather#ListCitiesOutput', traits: {"smithy.api#output" => {}}) + NoSuchResource = StructureShape.new(id: 'example.weather#NoSuchResource', traits: {"smithy.api#error" => "client"}) - CityCoordinates.add_member(:latitude, 'latitude', Prelude::Float, traits: {"smithy.api#required"=>{}}) - CityCoordinates.add_member(:longitude, 'longitude', Prelude::Float, traits: {"smithy.api#required"=>{}}) + CityCoordinates.add_member(:latitude, 'latitude', Prelude::Float, traits: {"smithy.api#required" => {}}) + CityCoordinates.add_member(:longitude, 'longitude', Prelude::Float, traits: {"smithy.api#required" => {}}) CityCoordinates.type = Types::CityCoordinates CitySummaries.set_member(CitySummary) - CitySummary.add_member(:city_id, 'cityId', CityId, traits: {"smithy.api#required"=>{}}) - CitySummary.add_member(:name, 'name', Prelude::String, traits: {"smithy.api#required"=>{}}) + CitySummary.add_member(:city_id, 'cityId', CityId, traits: {"smithy.api#required" => {}}) + CitySummary.add_member(:name, 'name', Prelude::String, traits: {"smithy.api#required" => {}}) CitySummary.type = Types::CitySummary - GetCityInput.add_member(:city_id, 'cityId', CityId, traits: {"smithy.api#required"=>{}}) + GetCityInput.add_member(:city_id, 'cityId', CityId, traits: {"smithy.api#required" => {}}) GetCityInput.type = Types::GetCityInput - GetCityOutput.add_member(:name, 'name', Prelude::String, traits: {"smithy.api#notProperty"=>{}, "smithy.api#required"=>{}}) - GetCityOutput.add_member(:coordinates, 'coordinates', CityCoordinates, traits: {"smithy.api#required"=>{}}) + GetCityOutput.add_member(:name, 'name', Prelude::String, traits: {"smithy.api#notProperty" => {}, "smithy.api#required" => {}}) + GetCityOutput.add_member(:coordinates, 'coordinates', CityCoordinates, traits: {"smithy.api#required" => {}}) GetCityOutput.type = Types::GetCityOutput - GetCurrentTimeOutput.add_member(:time, 'time', Prelude::Timestamp, traits: {"smithy.api#required"=>{}}) + GetCurrentTimeOutput.add_member(:time, 'time', Prelude::Timestamp, traits: {"smithy.api#required" => {}}) GetCurrentTimeOutput.type = Types::GetCurrentTimeOutput - GetForecastInput.add_member(:city_id, 'cityId', CityId, traits: {"smithy.api#required"=>{}}) + GetForecastInput.add_member(:city_id, 'cityId', CityId, traits: {"smithy.api#required" => {}}) GetForecastInput.type = Types::GetForecastInput GetForecastOutput.add_member(:chance_of_rain, 'chanceOfRain', Prelude::Float) GetForecastOutput.type = Types::GetForecastOutput @@ -42,22 +42,22 @@ module Schema ListCitiesInput.add_member(:page_size, 'pageSize', Prelude::Integer) ListCitiesInput.type = Types::ListCitiesInput ListCitiesOutput.add_member(:next_token, 'nextToken', Prelude::String) - ListCitiesOutput.add_member(:items, 'items', CitySummaries, traits: {"smithy.api#required"=>{}}) + ListCitiesOutput.add_member(:items, 'items', CitySummaries, traits: {"smithy.api#required" => {}}) ListCitiesOutput.type = Types::ListCitiesOutput - NoSuchResource.add_member(:resource_type, 'resourceType', Prelude::String, traits: {"smithy.api#required"=>{}}) + NoSuchResource.add_member(:resource_type, 'resourceType', Prelude::String, traits: {"smithy.api#required" => {}}) NoSuchResource.type = Types::NoSuchResource SERVICE = ServiceShape.new do |service| service.id = "example.weather#Weather" service.name = "Weather" service.version = "2006-03-01" - service.traits = {"smithy.api#paginated"=>{"inputToken"=>"nextToken", "outputToken"=>"nextToken", "pageSize"=>"pageSize"}} + service.traits = {"smithy.api#paginated" => {"inputToken" => "nextToken", "outputToken" => "nextToken", "pageSize" => "pageSize"}} service.add_operation(:get_city, OperationShape.new do |operation| operation.id = "example.weather#GetCity" operation.name = "GetCity" operation.input = GetCityInput operation.output = GetCityOutput - operation.traits = {"smithy.api#readonly"=>{}} + operation.traits = {"smithy.api#readonly" => {}} operation.errors << NoSuchResource end) service.add_operation(:get_current_time, OperationShape.new do |operation| @@ -65,21 +65,21 @@ module Schema operation.name = "GetCurrentTime" operation.input = Prelude::Unit operation.output = GetCurrentTimeOutput - operation.traits = {"smithy.api#readonly"=>{}} + operation.traits = {"smithy.api#readonly" => {}} end) service.add_operation(:get_forecast, OperationShape.new do |operation| operation.id = "example.weather#GetForecast" operation.name = "GetForecast" operation.input = GetForecastInput operation.output = GetForecastOutput - operation.traits = {"smithy.api#readonly"=>{}} + operation.traits = {"smithy.api#readonly" => {}} end) service.add_operation(:list_cities, OperationShape.new do |operation| operation.id = "example.weather#ListCities" operation.name = "ListCities" operation.input = ListCitiesInput operation.output = ListCitiesOutput - operation.traits = {"smithy.api#paginated"=>{"items"=>"items"}, "smithy.api#readonly"=>{}} + operation.traits = {"smithy.api#paginated" => {"items" => "items"}, "smithy.api#readonly" => {}} operation[:paginator] = Paginators::ListCities.new end) end diff --git a/projections/weather/spec/weather/endpoint_provider_spec.rb b/projections/weather/spec/weather/endpoint_provider_spec.rb index 5b7901ebd..226652f4c 100644 --- a/projections/weather/spec/weather/endpoint_provider_spec.rb +++ b/projections/weather/spec/weather/endpoint_provider_spec.rb @@ -10,11 +10,11 @@ module Weather context "Endpoint set" do let(:expected) do - {"endpoint"=>{"url"=>"https://example.com"}} + {"endpoint" => {"url" => "https://example.com"}} end it 'produces the expected output from the EndpointProvider' do - params = EndpointParameters.new(**{:endpoint=>"https://example.com"}) + params = EndpointParameters.new(**{endpoint: "https://example.com"}) endpoint = subject.resolve(params) expect(endpoint.uri).to eq(expected['endpoint']['url']) expect(endpoint.headers).to eq(expected['endpoint']['headers'] || {}) @@ -25,7 +25,7 @@ module Weather context "Endpoint not set" do let(:expected) do - {"error"=>"Endpoint is not set - you must configure an endpoint."} + {"error" => "Endpoint is not set - you must configure an endpoint."} end it 'produces the expected output from the EndpointProvider' do From b23a2e95cce8a92c3bfb45f15711bf4562127605 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 28 Apr 2025 09:19:10 -0700 Subject: [PATCH 35/54] Document now inherits SimpleDelegator --- gems/smithy-schema/lib/smithy-schema/document.rb | 14 ++++---------- .../lib/smithy-schema/document_utils.rb | 1 - .../spec/smithy-schema/type_registry_spec.rb | 1 - 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/gems/smithy-schema/lib/smithy-schema/document.rb b/gems/smithy-schema/lib/smithy-schema/document.rb index 81b01c035..fa92e24f1 100644 --- a/gems/smithy-schema/lib/smithy-schema/document.rb +++ b/gems/smithy-schema/lib/smithy-schema/document.rb @@ -10,7 +10,7 @@ module Schema # with a shape to serialize its contents. # # Smithy-Ruby currently only support JSON documents. - class Document + class Document < ::SimpleDelegator # @param [Object] data document data # @param [Hash] options # @option options [Smithy::Schema::Structure] :shape shape to reference when setting @@ -24,20 +24,14 @@ class Document def initialize(data, options = {}) @data = set_data(data, options) @discriminator = extract_discriminator(data, options) + super(@data) end - # @return [Object] data - attr_reader :data - # @return [String] discriminator attr_reader :discriminator - # @param [String] key - # @return [Object] - def [](key) - return unless @data.is_a?(Hash) && @data.key?(key) - - @data[key] + def data + __getobj__ # return object we are delegating to, required end # @param [Shapes::Structure] shape diff --git a/gems/smithy-schema/lib/smithy-schema/document_utils.rb b/gems/smithy-schema/lib/smithy-schema/document_utils.rb index c21a2d1c8..a889847b8 100644 --- a/gems/smithy-schema/lib/smithy-schema/document_utils.rb +++ b/gems/smithy-schema/lib/smithy-schema/document_utils.rb @@ -142,7 +142,6 @@ def extract_structure(data, shape, opts) member_shape = shape.member(k) member_name = resolve_member_name(member_shape, opts) - pp member_name o[member_name] = extract(v, member_shape, opts) end end diff --git a/gems/smithy-schema/spec/smithy-schema/type_registry_spec.rb b/gems/smithy-schema/spec/smithy-schema/type_registry_spec.rb index 37b979384..5cc1b618d 100644 --- a/gems/smithy-schema/spec/smithy-schema/type_registry_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/type_registry_spec.rb @@ -6,7 +6,6 @@ module Smithy module Schema describe TypeRegistry do - subject { TypeRegistry.new({ 'thing' => shape }) } let(:shape) { SchemaHelper.sample_schema.const_get(:Structure) } From 306a97e7c341c7007047ea18f70d5c6415bbb77e Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 28 Apr 2025 11:54:36 -0700 Subject: [PATCH 36/54] Expand on type registry docs --- gems/smithy-schema/lib/smithy-schema/type_registry.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/gems/smithy-schema/lib/smithy-schema/type_registry.rb b/gems/smithy-schema/lib/smithy-schema/type_registry.rb index bfb5c4609..00ef0116f 100644 --- a/gems/smithy-schema/lib/smithy-schema/type_registry.rb +++ b/gems/smithy-schema/lib/smithy-schema/type_registry.rb @@ -2,7 +2,9 @@ module Smithy module Schema - # A registry that contains a map of Smithy shape ID to the shape representation. + # A registry that contains a map of Smithy shape ID to its shape defined in a schema. + # The registered shapes are limited to{Shapes::StructureShape} and {Shapes::UnionShape} + # shapes, each with a type representation. # # This registry has the following functionalities: # @@ -11,7 +13,7 @@ module Schema # * Register shape to the Registry # * Supports enumeration of registered shapes # - # You could also combine multiple registries into one registry. + # You could also combine multiple registries into one {TypeRegistry}. # # @example Creating a new Registry # # accepts a map of id/shapes @@ -21,7 +23,7 @@ module Schema # ) # # @example Shape Lookup - # # Find shape by its shape id + # # Find shape by its id # registry["someId"] # # => # # From 8ef30554dec60918f3c20e4c7f80e6889921b170 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Tue, 29 Apr 2025 07:19:42 -0700 Subject: [PATCH 37/54] Fix bug in timehelper --- gems/smithy-schema/lib/smithy-schema/time_helper.rb | 4 ++-- gems/smithy-schema/spec/smithy-schema/time_helper_spec.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gems/smithy-schema/lib/smithy-schema/time_helper.rb b/gems/smithy-schema/lib/smithy-schema/time_helper.rb index e17634503..43ae61201 100644 --- a/gems/smithy-schema/lib/smithy-schema/time_helper.rb +++ b/gems/smithy-schema/lib/smithy-schema/time_helper.rb @@ -16,9 +16,9 @@ def time(time, trait) case trait when 'http-date' - time.utc.iso8601 - when 'date-time' time.utc.httpdate + when 'date-time' + time.utc.iso8601 when 'epoch-seconds' time.utc.to_i else diff --git a/gems/smithy-schema/spec/smithy-schema/time_helper_spec.rb b/gems/smithy-schema/spec/smithy-schema/time_helper_spec.rb index daa78d6f9..8ddad8930 100644 --- a/gems/smithy-schema/spec/smithy-schema/time_helper_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/time_helper_spec.rb @@ -10,11 +10,11 @@ module Schema let(:time) { Time.utc(2002, 10, 31) } it 'returns as http-date format' do - expect(subject.time(time, 'http-date')).to eq('2002-10-31T00:00:00Z') + expect(subject.time(time, 'http-date')).to eq('Thu, 31 Oct 2002 00:00:00 GMT') end it 'returns as date-time format' do - expect(subject.time(time, 'date-time')).to eq('Thu, 31 Oct 2002 00:00:00 GMT') + expect(subject.time(time, 'date-time')).to eq('2002-10-31T00:00:00Z') end it 'returns as epoch-seconds format' do From a297963a323cbd044fb25e4fdb0997320f648332 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Tue, 29 Apr 2025 09:16:19 -0700 Subject: [PATCH 38/54] Slim down the sample shapes --- gems/smithy-schema/spec/support/schema_helper.rb | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/gems/smithy-schema/spec/support/schema_helper.rb b/gems/smithy-schema/spec/support/schema_helper.rb index c4f925b06..6730445ee 100644 --- a/gems/smithy-schema/spec/support/schema_helper.rb +++ b/gems/smithy-schema/spec/support/schema_helper.rb @@ -89,17 +89,11 @@ def sample_shapes 'target' => 'smithy.ruby.tests#StreamingBlob', 'traits' => { 'smithy.api#default' => 'streamingBlob' } }, - 'string' => { - 'target' => 'smithy.api#String', - 'traits' => { 'smithy.api#jsonName' => 'jsonName' } - }, + 'string' => { 'target' => 'smithy.api#String' }, 'structure' => { 'target' => 'smithy.ruby.tests#Structure' }, 'structureList' => { 'target' => 'smithy.ruby.tests#StructureList' }, 'structureMap' => { 'target' => 'smithy.ruby.tests#StructureMap' }, - 'timestamp' => { - 'target' => 'smithy.api#Timestamp', - 'traits' => { 'smithy.api#timestampFormat' => 'http-date' } - }, + 'timestamp' => { 'target' => 'smithy.api#Timestamp' }, 'union' => { 'target' => 'smithy.ruby.tests#Union' } } }, From 8e2d32276465f24be3c94f746aae9985fd9b7424 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Tue, 29 Apr 2025 09:16:58 -0700 Subject: [PATCH 39/54] Update docs --- gems/smithy-schema/lib/smithy-schema/document.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gems/smithy-schema/lib/smithy-schema/document.rb b/gems/smithy-schema/lib/smithy-schema/document.rb index fa92e24f1..7eeab01f9 100644 --- a/gems/smithy-schema/lib/smithy-schema/document.rb +++ b/gems/smithy-schema/lib/smithy-schema/document.rb @@ -13,7 +13,7 @@ module Schema class Document < ::SimpleDelegator # @param [Object] data document data # @param [Hash] options - # @option options [Smithy::Schema::Structure] :shape shape to reference when setting + # @option options [Smithy::Schema::StructureShape] :shape shape to reference when setting # document data. Only applicable when data param is a type of {Shapes::StructureShape}. # @option options [Boolean] :use_timestamp_format Whether to use the `timestampFormat` # trait or ignore it when creating a {Document} with given shape. The `timestampFormat` @@ -31,7 +31,7 @@ def initialize(data, options = {}) attr_reader :discriminator def data - __getobj__ # return object we are delegating to, required + __getobj__ end # @param [Shapes::Structure] shape From cb66cec85461817fbdb49a32122fe5dc3aa5d5d9 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 5 May 2025 10:10:24 -0700 Subject: [PATCH 40/54] Only add structures to type registry --- gems/smithy/lib/smithy/views/client/schema.rb | 6 ++++++ projections/shapes/lib/shapes/schema.rb | 2 ++ projections/weather/lib/weather/schema.rb | 3 +++ 3 files changed, 11 insertions(+) diff --git a/gems/smithy/lib/smithy/views/client/schema.rb b/gems/smithy/lib/smithy/views/client/schema.rb index 4dd06f9a5..f1ad63feb 100644 --- a/gems/smithy/lib/smithy/views/client/schema.rb +++ b/gems/smithy/lib/smithy/views/client/schema.rb @@ -27,6 +27,11 @@ def shapes .map { |k, v| build_shape(k, v) } end + def typed_shapes + shapes.select { |s| s.type == 'structure' } + .map { |s| "'#{s.id}' => #{s.name}" } + end + def operation_shapes @operation_shapes ||= @service_index @@ -155,6 +160,7 @@ def initialize(service, id, shape) end attr_reader :type + attr_reader :id def name @service.dig('rename', @id) || Model::Shape.name(@id).camelize diff --git a/projections/shapes/lib/shapes/schema.rb b/projections/shapes/lib/shapes/schema.rb index ea4c281ef..2aa048b95 100644 --- a/projections/shapes/lib/shapes/schema.rb +++ b/projections/shapes/lib/shapes/schema.rb @@ -75,5 +75,7 @@ module Schema operation.traits = {"smithy.ruby.tests#shape" => {}} end) end + + TYPE_REGISTRY = Smithy::Schema::TypeRegistry.new('smithy.ruby.tests#OperationInputOutput' => OperationInputOutput, 'smithy.ruby.tests#Structure' => Structure) end end diff --git a/projections/weather/lib/weather/schema.rb b/projections/weather/lib/weather/schema.rb index 1ce8a9e34..73b383451 100644 --- a/projections/weather/lib/weather/schema.rb +++ b/projections/weather/lib/weather/schema.rb @@ -84,7 +84,10 @@ module Schema operation.output = ShapeRef.new(shape: ListCitiesOutput) # TODO: support parsing errors defined at the service level operation.traits = {"smithy.api#readonly" => {}} + operation[:paginator] = Paginators::ListCities.new end) end + + TYPE_REGISTRY = Smithy::Schema::TypeRegistry.new('example.weather#CityCoordinates' => CityCoordinates, 'example.weather#CitySummary' => CitySummary, 'example.weather#GetCityInput' => GetCityInput, 'example.weather#GetCityOutput' => GetCityOutput, 'example.weather#GetCurrentTimeOutput' => GetCurrentTimeOutput, 'example.weather#GetForecastInput' => GetForecastInput, 'example.weather#GetForecastOutput' => GetForecastOutput, 'example.weather#ListCitiesInput' => ListCitiesInput, 'example.weather#ListCitiesOutput' => ListCitiesOutput, 'example.weather#NoSuchResource' => NoSuchResource) end end From 9fa984cc410898e5b8d0e263e435060f0df95920 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 5 May 2025 10:23:14 -0700 Subject: [PATCH 41/54] Update TypeRegistry to limit to StructureShape --- .../lib/smithy-schema/type_registry.rb | 35 ++++++++----------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/gems/smithy-schema/lib/smithy-schema/type_registry.rb b/gems/smithy-schema/lib/smithy-schema/type_registry.rb index 00ef0116f..a76e9b272 100644 --- a/gems/smithy-schema/lib/smithy-schema/type_registry.rb +++ b/gems/smithy-schema/lib/smithy-schema/type_registry.rb @@ -3,8 +3,7 @@ module Smithy module Schema # A registry that contains a map of Smithy shape ID to its shape defined in a schema. - # The registered shapes are limited to{Shapes::StructureShape} and {Shapes::UnionShape} - # shapes, each with a type representation. + # The registered shapes are limited to {Shapes::StructureShape} with a type representation. # # This registry has the following functionalities: # @@ -37,39 +36,39 @@ module Schema class TypeRegistry include Enumerable - # @param [Hash] registry + # @param [Hash] registry def initialize(registry = {}) @registry = registry @shapes_by_type = register_shape_types(registry.values) end # @api private - # @return [Hash] + # @return [Hash] attr_accessor :registry # @api private - # @return [Hash] + # @return [Hash] attr_reader :shapes_by_type - # @return [Hash] + # @return [Hash] def each(&) @registry.each(&) end # @param [String] id - # @return [Shapes::Structure, nil] + # @return [Shapes::StructureShape, nil] def [](id) @registry[id] end # @param [String] id - # @param [Shapes::Structure] shape + # @param [Shapes::StructureShape] shape def []=(id, shape) - msg = 'Expected a shape with members and type' - raise ArgumentError, msg unless shape.is_a?(Shapes::Structure) && shape.type + msg = 'Expected a StructureShape that has a type representation' + raise ArgumentError, msg unless shape.is_a?(Shapes::StructureShape) && shape.type @registry[id] = shape - register_shape_type(shape, @shapes_by_type) + @shapes_by_type[shape.type] = shape end # Returns true if the registry contains specific shape id. @@ -89,7 +88,7 @@ def shape_by_type?(type) # Returns the shape registered for the given type. # @param [Class] type - # @return [Shapes::Structure, nil] + # @return [Shapes::StructureShape, nil] def shape_by_type(type) @shapes_by_type[type] end @@ -98,16 +97,10 @@ def shape_by_type(type) def register_shape_types(shapes) shapes.each_with_object({}) do |s, h| - register_shape_type(s, h) - end - end + msg = 'Expected a StructureShape that has a type representation' + raise ArgumentError, msg unless s.is_a?(Shapes::StructureShape) && s.type - def register_shape_type(shape, mapping) - case shape - when Shapes::StructureShape - mapping[shape.type] = shape - when Shapes::UnionShape - shape.member_types.values { |v| mapping[v] = shape } + h[s.type] = s end end From 166c8b1c364c3b1bb8f0be606b89b1c6b40e520c Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 5 May 2025 23:13:35 -0700 Subject: [PATCH 42/54] Document revamp --- .../lib/smithy-schema/document.rb | 103 +---- .../smithy-schema/document/deserializer.rb | 63 +++ .../lib/smithy-schema/document/serializer.rb | 213 ++++++++++ .../lib/smithy-schema/document_utils.rb | 216 ---------- .../spec/fixtures/typed-documents/model.json | 150 +++++++ .../fixtures/typed-documents/test-cases.json | 278 +++++++++++++ .../spec/smithy-schema/document_spec.rb | 390 +++++++++++------- gems/smithy/lib/smithy/views/client/schema.rb | 3 +- 8 files changed, 965 insertions(+), 451 deletions(-) create mode 100644 gems/smithy-schema/lib/smithy-schema/document/deserializer.rb create mode 100644 gems/smithy-schema/lib/smithy-schema/document/serializer.rb delete mode 100644 gems/smithy-schema/lib/smithy-schema/document_utils.rb create mode 100644 gems/smithy-schema/spec/fixtures/typed-documents/model.json create mode 100644 gems/smithy-schema/spec/fixtures/typed-documents/test-cases.json diff --git a/gems/smithy-schema/lib/smithy-schema/document.rb b/gems/smithy-schema/lib/smithy-schema/document.rb index 7eeab01f9..b59206074 100644 --- a/gems/smithy-schema/lib/smithy-schema/document.rb +++ b/gems/smithy-schema/lib/smithy-schema/document.rb @@ -1,94 +1,35 @@ # frozen_string_literal: true -require_relative 'document_utils' +require_relative 'document/deserializer' +require_relative 'document/serializer' module Smithy module Schema - # A Smithy document type, representing typed or untyped data from Smithy data model. - # ## Document types - # Document types are protocol-agnostic view of untyped data. They could be combined - # with a shape to serialize its contents. - # - # Smithy-Ruby currently only support JSON documents. - class Document < ::SimpleDelegator - # @param [Object] data document data - # @param [Hash] options - # @option options [Smithy::Schema::StructureShape] :shape shape to reference when setting - # document data. Only applicable when data param is a type of {Shapes::StructureShape}. - # @option options [Boolean] :use_timestamp_format Whether to use the `timestampFormat` - # trait or ignore it when creating a {Document} with given shape. The `timestampFormat` - # trait is ignored by default. - # @option options [Boolean] :use_json_name Whether to use the `jsonName` trait or ignore - # it when creating a {Document} with given shape. The `jsonName` trait is ignored - # by default. - def initialize(data, options = {}) - @data = set_data(data, options) - @discriminator = extract_discriminator(data, options) - super(@data) - end - - # @return [String] discriminator - attr_reader :discriminator - - def data - __getobj__ - end - - # @param [Shapes::Structure] shape - # @return [Object] typed shape - def as_typed(shape) - error_message = 'Invalid shape or document data' - raise ArgumentError, error_message unless valid_shape?(shape) && @data.is_a?(Hash) - - type = shape.type.new - DocumentUtils.apply(@data, shape, type) - end - - private - - def discriminator?(data) - data.is_a?(Hash) && data.key?('__type') - end - - def extract_discriminator(data, opts) - return if data.nil? - - return unless discriminator?(data) || (shape = opts[:shape]) - - if discriminator?(data) - data['__type'] - else - error_message = "Expected a structure shape, given #{shape.class} instead" - raise error_message unless valid_shape?(shape) - - shape.id + module Document + # A Smithy document type, representing typed or untyped data from Smithy data model. + # ## Document types + # Document types are protocol-agnostic view of untyped data. They could be combined + # with a shape to serialize its contents. + # + # Smithy-Ruby currently only support JSON documents. + class Data < ::SimpleDelegator + # @param [Object] data document data + # @param [Hash] options + # @option options [Smithy::Schema::StructureShape] :shape shape to reference when setting + # document data. + def initialize(data, options = {}) + @data = data + @discriminator = options[:discriminator] || nil + super(@data) end - end - - def set_data(data, opts) - return if data.nil? - case data - when Smithy::Schema::Structure - shape = opts[:shape] - if shape.nil? || !valid_shape?(shape) - raise ArgumentError, "Unable to create a document with given shape: #{shape}" - end + # @return [String] discriminator + attr_reader :discriminator - opts = opts.except(:shape) - DocumentUtils.extract(data, shape, opts) - else - if discriminator?(data) - data.except('__type') - else - DocumentUtils.format(data) - end + def data + __getobj__ end end - - def valid_shape?(shape) - shape.is_a?(Shapes::StructureShape) && !shape.type.nil? - end end end end diff --git a/gems/smithy-schema/lib/smithy-schema/document/deserializer.rb b/gems/smithy-schema/lib/smithy-schema/document/deserializer.rb new file mode 100644 index 000000000..3b1ef9c14 --- /dev/null +++ b/gems/smithy-schema/lib/smithy-schema/document/deserializer.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Smithy + module Schema + module Document + # TODO + class Deserializer + def initialize(type_registry) + @type_registry = type_registry + end + + # TODO......... + def deserialize(document, shape: nil) + # TODO + end + + private + + def valid_shape?(shape) + shape.is_a?(Shapes::StructureShape) && !shape.type.nil? + end + + # Apply data into a given runtime shape + def parse(ref, value, type) # rubocop:disable Metrics/CyclomaticComplexity + case ref.shape + when Shapes::StructureShape then structure(ref, value, type) + when Shapes::UnionShape then union(ref, value, type) + when Shapes::ListShape then list(ref, value, type) + when Shapes::MapShape then map(ref, value, type) + when Shapes::TimestampShape then timestamp(ref, value, type) + when Shapes::DocumentShape then document(ref, value, type) + when Shapes::BlobShape then Base64.strict_decode64(value) + else data + end + end + + def document(ref, value, type = nil) + # TODO + end + + def list(ref, value, type = nil) + # TODO + end + + def map(ref, values, type = nil) + # TODO + end + + def structure(ref, values, type = nil) + # TODO + end + + def timestamp(ref, values, type = nil) + # TODO + end + + def union(ref, values, type = nil) + # TODO + end + end + end + end +end diff --git a/gems/smithy-schema/lib/smithy-schema/document/serializer.rb b/gems/smithy-schema/lib/smithy-schema/document/serializer.rb new file mode 100644 index 000000000..a63848160 --- /dev/null +++ b/gems/smithy-schema/lib/smithy-schema/document/serializer.rb @@ -0,0 +1,213 @@ +# frozen_string_literal: true + +require 'base64' +require 'time' + +module Smithy + module Schema + module Document + # TODO + class Serializer + def initialize(type_registry) + @type_registry = type_registry + end + + # data can come in several forms + # - ruby objects w/o attachments with schema + # - instance of runtime shape (requires shape to properly serialize) + # - json response that includes discriminator (requires shape to properly serialize) + def create_document(data) + raise ArgumentError, 'Unable to create Document' if data.nil? + + case data + when Smithy::Schema::Structure # runtime shape + msg = 'Given runtime shape not found in type registry' + raise ArgumentError, msg unless @type_registry.shape_by_type?(data.class) + + shape = @type_registry.shape_by_type(data.class) + ref = Smithy::Schema::Shapes::ShapeRef.new(shape: shape) + new_data = build(ref, data) + new_data['__type'] = shape.id + Data.new(new_data, discriminator: shape.id) + else + # shape is typed + if discriminator?(data) + msg = 'Given discriminator not found in type registry' + raise ArgumentError, msg unless @type_registry.key?(data['__type']) + + discriminator = data['__type'] + shape = @type_registry[discriminator] + ref = Smithy::Schema::Shapes::ShapeRef.new(shape: shape) + new_data = build(ref, data) + new_data['__type'] = shape.id + Data.new(new_data, discriminator: shape.id) + else + Data.new(build_untyped(data)) + end + end + end + + def serialize_document(document, opts = {}) + error_message = 'Invalid Document - must be a typed document' + raise ArgumentError, error_message unless document.is_a?(Data) && document.discriminator + + opts[:discriminator] = true + shape = @type_registry[document.discriminator] + ref = Smithy::Schema::Shapes::ShapeRef.new(shape: shape) + new_data = build(ref, document.data, opts) + new_data['__type'] = shape.id + new_data + end + + private + + def discriminator?(data) + data.is_a?(Hash) && data.key?('__type') + end + + # Takes untyped ruby data into document-friendly format + def build_untyped(values) + return if values.nil? + + case values + when Time + values.to_i # timestamp format is "epoch-seconds" by default + when Hash + values.each_with_object({}) do |(k, v), h| + h[k.to_s] = build_untyped(v) + end + when Array + values.map { |d| build_untyped(d) } + else + values + end + end + + # Construct data into a document-friendly format using a shape + def build(ref, values, opts = {}) # rubocop:disable Metrics/CyclomaticComplexity + return if values.nil? + + case ref.shape + when Shapes::StructureShape then structure(ref, values, opts) + when Shapes::UnionShape then union(ref, values, opts) + when Shapes::ListShape then list(ref, values, opts) + when Shapes::MapShape then map(ref, values, opts) + when Shapes::BlobShape then blob(values, opts) + when Shapes::FloatShape then float(values, opts) + when Shapes::TimestampShape then timestamp(ref, values, opts) + when Shapes::DocumentShape then document(values, opts) + else values + end + end + + def structure(ref, values, opts) + values.each_pair.with_object({}) do |(k, v), h| + next if v.nil? + + member_ref = ref.shape.member(k) || member_by_location_name(ref.shape, k) + next if member_ref.nil? + + member_name = + if opts[:use_json_name] && member_ref.traits['smithy.api#jsonName'] + member_ref.traits['smithy.api#jsonName'] + else + member_ref.location_name + end + h[member_name] = build(member_ref, v, opts) + end + end + + def member_by_location_name(shape, name) + shape.members.values.find { |ref| ref.location_name == name } + end + + def union(ref, values, opts) + data = {} + if values.is_a?(Smithy::Schema::Union) + member_ref = ref.shape.member_by_type(values.class) + member_name = + if opts[:use_json_name] && member_ref.traits['smithy.api#jsonName'] + member_ref.traits['smithy.api#jsonName'] + else + member_ref.location_name + end + data[member_name] = build(member_ref, values, opts) + else + key, value = values.first + member_ref = ref.shape.member(key) || member_by_location_name(ref.shape, key) + unless member_ref.nil? + member_name = + if opts[:use_json_name] && member_ref.traits['smithy.api#jsonName'] + member_ref.traits['smithy.api#jsonName'] + else + member_ref.location_name + end + data[member_name] = build(member_ref, value, opts) + end + end + data + end + + def list(ref, values, opts) + values.collect do |value| + next if value.nil? + + build(ref.shape.member, value, opts) + end + end + + def map(ref, values, opts) + values.each.with_object({}) do |(key, value), data| + next if value.nil? + + data[key.to_s] = build(ref.shape.value, value, opts) + end + end + + def blob(value, opts) + return value if opts[:discriminator] # blob is already encoded + + Base64.strict_encode64(value.is_a?(String) ? value : value.read) + end + + def timestamp(ref, value, opts) + return value.to_i unless opts[:use_timestamp_format] + + trait = 'smithy.api#timestampFormat' + value = value.is_a?(Numeric) ? Time.at(value) : Time.parse(value) unless value.is_a?(Time) + case ref.traits[trait] || ref.shape.traits[trait] + when 'date-time' then value.utc.iso8601 + when 'http-date' then value.utc.httpdate + else + # default to epoch-seconds + value.to_i + end + end + + def float(value, _opts) + if value == ::Float::INFINITY + 'Infinity' + elsif value == -::Float::INFINITY + '-Infinity' + elsif value.nan? + 'NaN' + else + value + end + end + + def document(values, opts) + if values.is_a?(Smithy::Schema::Structure) + shape = @type_registry.shape_by_type(values.class) + shape_ref = Smithy::Schema::Shapes::ShapeRef.new(shape: shape) + data = build(shape_ref, values, opts) + data['__type'] = shape.id + data + else + values + end + end + end + end + end +end diff --git a/gems/smithy-schema/lib/smithy-schema/document_utils.rb b/gems/smithy-schema/lib/smithy-schema/document_utils.rb deleted file mode 100644 index a889847b8..000000000 --- a/gems/smithy-schema/lib/smithy-schema/document_utils.rb +++ /dev/null @@ -1,216 +0,0 @@ -# frozen_string_literal: true - -require 'base64' -require 'time' - -module Smithy - module Schema - # @api private - # Document Utilities to help (de)construct data to/from Smithy document - module DocumentUtils - class << self - # Used to transform untyped data - def format(data) - return if data.nil? - - case data - when Time - data.to_i # timestamp format is "epoch-seconds" by default - when Hash - data.each_with_object({}) do |(k, v), h| - h[k.to_s] = format(v) - end - when Array - data.map { |d| format(d) } - else - data - end - end - - # Used to apply data to runtime shape - def apply(data, shape, type = nil) - case shape_reference(shape) - when Shapes::StructureShape then apply_structure(data, shape, type) - when Shapes::UnionShape then apply_union(data, shape, type) - when Shapes::ListShape then apply_list(data, shape) - when Shapes::MapShape then apply_map(data, shape) - when Shapes::TimestampShape then apply_timestamp(data, shape) - when Shapes::BlobShape then Base64.decode64(data) - else data - end - end - - # rubocop:disable Metrics/CyclomaticComplexity - def extract(data, shape, opts = {}) - return if data.nil? - - case shape_reference(shape) - when Shapes::StructureShape then extract_structure(data, shape, opts) - when Shapes::UnionShape then extract_union(data, shape, opts) - when Shapes::ListShape then extract_list(data, shape) - when Shapes::MapShape then extract_map(data, shape) - when Shapes::BlobShape then extract_blob(data) - when Shapes::TimestampShape then extract_timestamp(data, shape, opts) - else data - end - end - # rubocop:enable Metrics/CyclomaticComplexity - - private - - def apply_list(data, shape) - shape = shape_reference(shape) - data.map do |v| - next if v.nil? - - apply(v, shape.member) - end - end - - def apply_map(data, shape) - shape = shape_reference(shape) - data.transform_values do |v| - if v.nil? - nil - else - apply(v, shape.value) - end - end - end - - def apply_structure(data, shape, type) - shape = shape_reference(shape) - - type = shape.type.new if type.nil? - data.each do |k, v| - name = - if (member = member_with_json_name(k, shape)) - shape.name_by_member_name(member.name) - else - member_name(shape, k) - end - next if name.nil? - - type[name] = apply(v, shape.member(name)) - end - type - end - - def apply_timestamp(data, shape) - data = data.is_a?(Numeric) ? Time.at(data) : Time.parse(data) - TimeHelper.time(data, timestamp_format(shape)) - end - - def apply_union(data, shape, type) - shape = shape_reference(shape) - key, value = data.flatten - return if key.nil? - - if (member = member_with_json_name(key, shape)) - apply_union_member(member.name, value, shape, type) - elsif shape.name_by_member_name?(key) - apply_union_member(key, value, shape, type) - else - shape.member_type(:unknown).new(key, value) - end - end - - def apply_union_member(key, value, shape, type) - member_name = shape.name_by_member_name(key) - type = shape.member_type(member_name) if type.nil? - type.new(apply(value, shape.member(member_name))) - end - - def extract_blob(data) - Base64.strict_encode64(data.is_a?(String) ? data : data.read) - end - - def extract_list(data, shape) - shape = shape_reference(shape) - data.collect { |v| extract(v, shape.member) } - end - - def extract_map(data, shape) - shape = shape_reference(shape) - data.each.with_object({}) { |(k, v), h| h[k.to_s] = extract(v, shape.value) } - end - - def extract_structure(data, shape, opts) - shape = shape_reference(shape) - data.to_h.each_with_object({}) do |(k, v), o| - next unless shape.member?(k) - - member_shape = shape.member(k) - member_name = resolve_member_name(member_shape, opts) - o[member_name] = extract(v, member_shape, opts) - end - end - - def extract_timestamp(data, shape, opts) - return unless data.is_a?(Time) - - trait = opts[:use_timestamp_format] ? timestamp_format(shape) : 'epoch-seconds' - TimeHelper.time(data, trait) - end - - # rubocop:disable Metrics/AbcSize - def extract_union(data, shape, opts) - h = {} - shape = shape_reference(shape) - if data.is_a?(Schema::Union) - member_shape = shape.member_by_type(data.class) - member_name = resolve_member_name(member_shape, opts) - h[member_name] = extract(data, member_shape).value - else - key, value = data.first - if shape.member?(key) - member_shape = shape.member(key) - member_name = resolve_member_name(member_shape, opts) - h[member_name] = extract(value, member_shape) - end - end - h - end - # rubocop:enable Metrics/AbcSize - - def member_name(shape, key) - return unless shape.name_by_member_name?(key) || shape.member?(key.to_sym) - - shape.name_by_member_name(key) || key.to_sym - end - - def member_with_json_name(name, shape) - shape.members.values.find do |v| - v.traits['smithy.api#jsonName'] == name if v.traits.include?('smithy.api#jsonName') - end - end - - def resolve_member_name(member_shape, opts) - if opts[:use_json_name] && member_shape.traits['smithy.api#jsonName'] - member_shape.traits['smithy.api#jsonName'] - else - member_shape.name - end - end - - def shape_reference(shape) - shape.is_a?(Shapes::MemberShape) ? shape.shape : shape - end - - # The following steps are taken to determine the format of timestamp: - # Use the timestampFormat trait of the member, if present. - # Use the timestampFormat trait of the shape, if present. - # If none of the above applies, use epoch-seconds as default - def timestamp_format(shape) - if shape.traits['smithy.api#timestampFormat'] - shape.traits['smithy.api#timestampFormat'] - elsif shape.shape.traits['smithy.api#timestampFormat'] - shape.shape.traits['smithy.api#timestampFormat'] - else - 'epoch-seconds' - end - end - end - end - end -end diff --git a/gems/smithy-schema/spec/fixtures/typed-documents/model.json b/gems/smithy-schema/spec/fixtures/typed-documents/model.json new file mode 100644 index 000000000..f5cd7df14 --- /dev/null +++ b/gems/smithy-schema/spec/fixtures/typed-documents/model.json @@ -0,0 +1,150 @@ +{ + "smithy": "2.0", + "shapes": { + "smithy.ruby.tests#SampleSchema": { + "type" : "service", + "operations" : [ + { "target" : "smithy.example#Operation" } + ] + }, + "smithy.example#Operation": { + "type": "operation", + "input": { + "target": "smithy.example#OmniWidget" + }, + "output": { + "target": "smithy.example#OmniWidget" + } + }, + "smithy.example#OmniWidget": { + "type": "structure", + "members": { + "blob": { + "target": "smithy.api#Blob" + }, + "boolean": { + "target": "smithy.api#Boolean" + }, + "string": { + "target": "smithy.api#String", + "traits": { + "smithy.api#jsonName": "String", + "smithy.api#xmlName": "String" + } + }, + "byte": { + "target": "smithy.api#Byte" + }, + "short": { + "target": "smithy.api#Short" + }, + "integer": { + "target": "smithy.api#Integer" + }, + "long": { + "target": "smithy.api#Long" + }, + "float": { + "target": "smithy.api#Float" + }, + "double": { + "target": "smithy.api#Double" + }, + "bigInteger": { + "target": "smithy.api#BigInteger" + }, + "bigDecimal": { + "target": "smithy.api#BigDecimal" + }, + "timestamp": { + "target": "smithy.api#Timestamp" + }, + "timestampDateTime": { + "target": "smithy.api#Timestamp", + "traits": { + "smithy.api#timestampFormat": "date-time" + } + }, + "timestampHttpDate": { + "target": "smithy.api#Timestamp", + "traits": { + "smithy.api#timestampFormat": "http-date" + } + }, + "timestampEpochSeconds": { + "target": "smithy.api#Timestamp", + "traits": { + "smithy.api#timestampFormat": "epoch-seconds" + } + }, + "document": { + "target": "smithy.api#Document" + }, + "enum": { + "target": "smithy.example#ABEnum" + }, + "intEnum": { + "target": "smithy.example#ABIntEnum" + }, + "list": { + "target": "smithy.example#OmniWidgetList" + }, + "map": { + "target": "smithy.example#OmniWidgetMap" + }, + "structure": { + "target": "smithy.example#OmniWidget" + } + } + }, + "smithy.example#OmniWidgetList": { + "type": "list", + "member": { + "target": "smithy.example#OmniWidget" + } + }, + "smithy.example#OmniWidgetMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.example#OmniWidget" + } + }, + "smithy.example#ABEnum": { + "type": "enum", + "members": { + "A": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "A" + } + }, + "B": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "B" + } + } + } + }, + "smithy.example#ABIntEnum": { + "type": "intEnum", + "members": { + "A": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": 0 + } + }, + "B": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": 1 + } + } + } + } + } +} \ No newline at end of file diff --git a/gems/smithy-schema/spec/fixtures/typed-documents/test-cases.json b/gems/smithy-schema/spec/fixtures/typed-documents/test-cases.json new file mode 100644 index 000000000..6fabdc750 --- /dev/null +++ b/gems/smithy-schema/spec/fixtures/typed-documents/test-cases.json @@ -0,0 +1,278 @@ +{ + "serdeTests": [ + { + "name": "default test case", + "subject": "smithy.example#OmniWidget", + "serialized": { + "__type": "smithy.example#OmniWidget", + "string": "hello" + }, + "deserialized": { + "string": "hello" + }, + "codec": "json", + "settings": { + "jsonName": false + } + }, + { + "name": "jsonName", + "subject": "smithy.example#OmniWidget", + "serialized": { + "__type": "smithy.example#OmniWidget", + "String": "hello" + }, + "deserialized": { + "string": "hello" + }, + "codec": "json", + "settings": { + "jsonName": true + } + }, + { + "name": "timestampFormat, with default epoch-seconds", + "subject": "smithy.example#OmniWidget", + "serialized": { + "__type": "smithy.example#OmniWidget", + "timestamp": 0, + "timestampDateTime": "1970-01-01T00:00:00Z", + "timestampHttpDate": "Thu, 01 Jan 1970 00:00:00 GMT", + "timestampEpochSeconds": 0 + }, + "deserialized": { + "timestamp": "1970-01-01T00:00:00Z", + "timestampDateTime": "1970-01-01T00:00:00Z", + "timestampHttpDate": "1970-01-01T00:00:00Z", + "timestampEpochSeconds": "1970-01-01T00:00:00Z" + }, + "codec": "json", + "settings": { + "timestampFormat": { + "useTrait": true, + "default": "epoch-seconds" + } + } + }, + { + "name": "ignoring timestampFormat, with default epoch-seconds", + "subject": "smithy.example#OmniWidget", + "serialized": { + "__type": "smithy.example#OmniWidget", + "timestamp": 0, + "timestampDateTime": 0, + "timestampHttpDate": 0, + "timestampEpochSeconds": 0 + }, + "deserialized": { + "timestamp": "1970-01-01T00:00:00Z", + "timestampDateTime": "1970-01-01T00:00:00Z", + "timestampHttpDate": "1970-01-01T00:00:00Z", + "timestampEpochSeconds": "1970-01-01T00:00:00Z" + }, + "codec": "json", + "settings": { + "timestampFormat": { + "useTrait": false, + "default": "epoch-seconds" + } + } + }, + { + "name": "default settings with populated members with initial values", + "subject": "smithy.example#OmniWidget", + "serialized": { + "__type": "smithy.example#OmniWidget", + "blob": "YWJjZA==", + "boolean": false, + "String": "", + "byte": 0, + "short": 0, + "integer": 0, + "long": 0, + "float": 0, + "double": 0, + "bigInteger": 0, + "bigDecimal": 0, + "timestamp": 0, + "timestampDateTime": "1970-01-01T00:00:00Z", + "timestampHttpDate": "Thu, 01 Jan 1970 00:00:00 GMT", + "timestampEpochSeconds": 0, + "document": {}, + "enum": "A", + "intEnum": 0, + "list": [], + "map": {}, + "structure": {} + }, + "deserialized": { + "blob": "YWJjZA==", + "boolean": false, + "string": "", + "byte": 0, + "short": 0, + "integer": 0, + "long": 0, + "float": 0.0, + "double": 0.0, + "bigInteger": 0, + "bigDecimal": 0, + "timestamp": 0, + "timestampDateTime": "1970-01-01T00:00:00Z", + "timestampHttpDate": "Thu, 01 Jan 1970 00:00:00 GMT", + "timestampEpochSeconds": 0, + "document": {}, + "enum": "A", + "intEnum": 0, + "list": [], + "map": { + }, + "structure": { + } + }, + "codec": "json", + "settings": { + "defaultNamespace": "smithy.example", + "jsonName": true, + "timestampFormat": { + "useTrait": true, + "default": "epoch-seconds" + } + } + }, + { + "name": "default settings with populated members with filled values", + "subject": "smithy.example#OmniWidget", + "serialized": { + "__type": "smithy.example#OmniWidget", + "blob": "YWJjZA==", + "boolean": true, + "string": "abcd", + "byte": -128, + "short": -32768, + "integer": -2147483648, + "long": -9223372036854775808, + "float": -0.1, + "double": -0.01, + "bigInteger": -9223372036854775809, + "bigDecimal": -9223372036854775809.1, + "timestamp": 17514144000, + "timestampDateTime": "2525-01-01T00:00:00Z", + "timestampHttpDate": "Mon, 01 Jan 2525 00:00:00 GMT", + "timestampEpochSeconds": 17514144000, + "document": { + "__type": "smithy.example#OmniWidget", + "timestamp": 17514144000, + "timestampDateTime": "2525-01-01T00:00:00Z", + "timestampHttpDate": "Mon, 01 Jan 2525 00:00:00 GMT", + "timestampEpochSeconds": 17514144000, + "document": true + }, + "enum": "B", + "intEnum": 1, + "list": [ + { + "byte": -128 + }, + { + "short": -32768 + } + ], + "map": { + "a": { + "integer": -2147483648 + }, + "b": { + "long": -9223372036854775808 + } + }, + "structure": { + "list": [ + { + "double": -0.01 + }, + { + "float": -0.1 + } + ], + "map": { + "a": { + "bigInteger": -9223372036854775809 + }, + "b": { + "bigDecimal": -9223372036854775809.1 + } + } + } + }, + "deserialized": { + "blob": "YWJjZA==", + "boolean": true, + "string": "abcd", + "byte": -128, + "short": -32768, + "integer": -2147483648, + "long": -9223372036854775808, + "float": -0.1, + "double": -0.01, + "bigInteger": -9223372036854775809, + "bigDecimal": -9223372036854775809.1, + "timestamp": "2525-01-01T00:00:00Z", + "timestampDateTime": "2525-01-01T00:00:00Z", + "timestampHttpDate": "2525-01-01T00:00:00Z", + "timestampEpochSeconds": "2525-01-01T00:00:00Z", + "document": { + "timestamp": "2525-01-01T00:00:00Z", + "timestampDateTime": "2525-01-01T00:00:00Z", + "timestampHttpDate": "2525-01-01T00:00:00Z", + "timestampEpochSeconds": "2525-01-01T00:00:00Z" + }, + "enum": "B", + "intEnum": 1, + "list": [ + { + "byte": -128 + }, + { + "short": -32768 + } + ], + "map": { + "a": { + "integer": -2147483648 + }, + "b": { + "long": -9223372036854775808 + } + }, + "structure": { + "list": [ + { + "double": -0.01 + }, + { + "float": -0.1 + } + ], + "map": { + "a": { + "bigInteger": -9223372036854775809 + }, + "b": { + "bigDecimal": -9223372036854775809.1 + } + } + } + }, + "codec": "json", + "settings": { + "defaultNamespace": "smithy.example", + "jsonName": true, + "timestampFormat": { + "useTrait": true, + "default": "epoch-seconds" + } + } + } + ] +} \ No newline at end of file diff --git a/gems/smithy-schema/spec/smithy-schema/document_spec.rb b/gems/smithy-schema/spec/smithy-schema/document_spec.rb index 1590deac0..7fe10965e 100644 --- a/gems/smithy-schema/spec/smithy-schema/document_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/document_spec.rb @@ -5,49 +5,26 @@ module Smithy module Schema - describe Document do - let(:structure_shape) { SchemaHelper.sample_schema.const_get(:Structure) } - - let(:simple_schema) do - shape = Shapes::StructureShape.new(id: 'smithy.ruby.tests#SimpleStructure') - string = Shapes::StringShape.new(id: 'smithy.api#String') - shape.add_member(:string, 'string', string) - shape.type = simple_runtime - shape - end - - let(:simple_runtime) do - Struct.new(:string, keyword_init: true) do - include Smithy::Schema::Structure - end - end - - context 'untyped document' do - subject { Document.new(foo: 'bar') } + module Document + describe Data do + subject { Data.new({ 'foo' => 'bar' }) } describe '#initialize' do it 'sets data' do expect(subject.data).to eq('foo' => 'bar') end - it 'sets time data using default format' do - doc = Document.new(Time.utc(2024, 12, 25)) - expect(doc.data).to eq(1_735_084_800) - end - it 'defaults discriminator to nil' do expect(subject.discriminator).to be_nil end end describe '#[]' do - subject { Document.new({ foo: 'bar' }) } - it 'returns member value' do expect(subject['foo']).to eq('bar') end - it 'returns nil when member key is not applicable' do + it 'returns nil when member is not applicable' do expect(subject['bar']).to be_nil end end @@ -57,168 +34,277 @@ module Schema expect(subject.discriminator).to be_nil end end + end - describe '#as_typed' do - it 'converts document as runtime shape' do - typed_shape = Document.new({ string: 'foo' }).as_typed(structure_shape) - expect(typed_shape).to be_a(structure_shape.type) - expect(typed_shape[:string]).to eq('foo') - end + describe Serializer do + let(:shapes) { SchemaHelper.sample_shapes } + let(:sample_schema) { SchemaHelper.sample_schema(shapes: shapes) } + let(:type_registry) { sample_schema.const_get(:TYPE_REGISTRY) } + let(:structure_shape) { sample_schema.const_get(:Structure) } + let(:typed_shape) do + structure_shape.type.new( + big_decimal: 0, + big_integer: 0, + blob: StringIO.new('foo'), + boolean: true, + byte: 1, + document: true, + double: 1.1, + float: 1.1, + enum: 'enum', + int_enum: 0, + integer: 1, + long: 1, + short: 1, + list: %w[Item1 Item2], + map: { color: 'red' }, + streaming_blob: 'streaming blob', + string: 'foo', + structure_list: [{ integer: 1 }, { integer: 2 }, { integer: 3 }], + structure_map: { 'key' => { map: { 'color' => 'blue' } } }, + timestamp: Time.utc(2024, 12, 25), + union: { string: 'string' } + ) + end - it 'raises when invalid schema is given' do - invalid_schema = Shapes::StringShape.new(id: 'smithy.api#Invalid') - expect do - subject.as_typed(invalid_schema) - end.to raise_error(ArgumentError) - end + let(:expected_typed_data) do + { + '__type' => 'smithy.ruby.tests#Structure', + 'bigDecimal' => 0, + 'bigInteger' => 0, + 'blob' => 'Zm9v', + 'boolean' => true, + 'byte' => 1, + 'document' => true, + 'double' => 1.1, + 'float' => 1.1, + 'enum' => 'enum', + 'intEnum' => 0, + 'integer' => 1, + 'long' => 1, + 'short' => 1, + 'list' => %w[Item1 Item2], + 'map' => { 'color' => 'red' }, + 'streamingBlob' => 'c3RyZWFtaW5nIGJsb2I=', + 'string' => 'foo', + 'structureList' => [{ 'integer' => 1 }, { 'integer' => 2 }, { 'integer' => 3 }], + 'structureMap' => { 'key' => { 'map' => { 'color' => 'blue' } } }, + 'timestamp' => 1_735_084_800, + 'union' => { 'string' => 'string' } + } end - end - context 'typed document' do - context 'when runtime shape is the input' do - let(:typed_shape) do - structure_shape.type.new( - big_decimal: 0, - big_integer: 0, - blob: StringIO.new('foo'), - boolean: true, - byte: 1, - double: 1.1, - float: 1.1, - enum: 'enum', - int_enum: 0, - integer: 1, - long: 1, - short: 1, - list: %w[Item1 Item2], - map: { color: 'red' }, - streaming_blob: 'streaming blob', - string: 'foo', - structure_list: [{ integer: 1 }, { integer: 2 }, { integer: 3 }], - structure_map: { 'key' => { map: { 'color' => 'blue' } } }, - timestamp: Time.utc(2024, 12, 25), - union: { string: 'string' } - ) - end + subject { Document::Serializer.new(type_registry) } + let(:typed_document) { subject.create_document(typed_shape) } + let(:untyped_document) { subject.create_document(foo: 'bar') } - subject { Document.new(typed_shape, shape: structure_shape) } + describe '#create_document' do + context 'untyped input' do + it 'returns a document data' do + expect(untyped_document).to be_a_kind_of(Data) + end - describe '#initialize' do - it 'set data' do - expect(subject.data).to include( - { - 'bigDecimal' => 0, - 'bigInteger' => 0, - 'blob' => 'Zm9v', - 'boolean' => true, - 'byte' => 1, - 'double' => 1.1, - 'float' => 1.1, - 'enum' => 'enum', - 'intEnum' => 0, - 'integer' => 1, - 'long' => 1, - 'short' => 1, - 'list' => %w[Item1 Item2], - 'map' => { 'color' => 'red' }, - 'streamingBlob' => 'c3RyZWFtaW5nIGJsb2I=', - 'string' => 'foo', - 'structureList' => [{ 'integer' => 1 }, { 'integer' => 2 }, { 'integer' => 3 }], - 'structureMap' => { 'key' => { 'map' => { 'color' => 'blue' } } }, - 'timestamp' => 1_735_084_800, - 'union' => { 'string' => 'string' } - } - ) + it 'sets data' do + expect(untyped_document.data).to eq('foo' => 'bar') end - it 'set data using jsonName when applicable' do - typed_shape = structure_shape.type.new(string: 'foo', union: { string: 'bar' }) - doc = Document.new(typed_shape, shape: structure_shape, use_json_name: true) - expect(doc.data).to include({ 'jsonName' => 'foo', 'union' => { 'jsonName' => 'bar' } }) + it 'sets discriminator to nil' do + expect(untyped_document.discriminator).to be_nil end - it 'set data using timestampTrait when applicable' do - doc = Document.new(typed_shape, shape: structure_shape, use_timestamp_format: true) - expect(doc.data['timestamp']).to eq('2024-12-25T00:00:00Z') + it 'sets time data using default timestamp format' do + doc = subject.create_document(Time.utc(2024, 12, 25)) + expect(doc.data).to eq(1_735_084_800) end + end - it 'set discriminator' do - expect(subject.discriminator).to be(structure_shape.id) + context 'runtime shape input' do + let(:invalid_runtime) do + Struct.new(:string, keyword_init: true) do + include Smithy::Schema::Structure + end end - it 'raises when no schema is given' do - expect do - Document.new(typed_shape) - end.to raise_error(ArgumentError) + it 'sets data' do + expect(typed_document.data).to include(expected_typed_data) end - it 'raises when an invalid schema is provided' do - invalid_schema = Shapes::StringShape.new(id: 'smithy.api#String') + it 'sets discriminator' do + expect(typed_document.discriminator).to eql(structure_shape.id) + end + + it 'raises when runtime shape is not found in type registry' do expect do - Document.new(typed_shape, invalid_schema) + subject.create_document(invalid_runtime.new(string: 'foo')) end.to raise_error(ArgumentError) end end - describe '#as_typed' do - it 'converts document as a runtime shape' do - typed_shape = subject.as_typed(structure_shape) - expect(typed_shape.to_h).to include( + context 'parsed json input' do + let(:json) do + { + '__type' => 'smithy.ruby.tests#Structure', + 'string' => 'hello' + } + end + + let(:document) { subject.create_document(json) } + + it 'sets data' do + expect(document.data).to include( { - big_decimal: 0, - big_integer: 0, - blob: 'foo', - boolean: true, - byte: 1, - double: 1.1, - float: 1.1, - enum: 'enum', - int_enum: 0, - integer: 1, - long: 1, - short: 1, - string: 'foo', - streaming_blob: 'streaming blob', - structure_list: [{ integer: 1 }, { integer: 2 }, { integer: 3 }], - structure_map: { 'key' => { map: { 'color' => 'blue' } } }, - union: { string: 'string' }, - timestamp: '2024-12-25T00:00:00Z' + '__type' => 'smithy.ruby.tests#Structure', + 'string' => 'hello' } ) end - it 'converts document as a runtime shape of a similar schema' do - typed_shape = subject.as_typed(simple_schema) - expect(typed_shape).to be_a(simple_runtime) - expect(typed_shape[:string]).to eq('foo') + it 'sets discriminator' do + expect(document.discriminator).to eql(structure_shape.id) + end + + it 'raises when discriminator is not found in type registry' do + json['__type'] = 'smithy.ruby.tests#Invalid' + expect do + subject.create_document(json) + end.to raise_error(ArgumentError) end end end - context 'when parsed json is the input' do - let(:json) { <<~JSON.strip } - { - "__type": "foo.example#string", - "stringMember": "hello" + describe '#serialize_document' do + it 'returns serialized data' do + expect(subject.serialize_document(typed_document)) + .to include(expected_typed_data) + end + + it 'returns serialized data with jsonName when applicable' do + shapes['smithy.ruby.tests#Structure']['members']['string']['traits'] = { 'smithy.api#jsonName' => 'A' } + shapes['smithy.ruby.tests#Union']['members']['string']['traits'] = { 'smithy.api#jsonName' => 'B' } + + document = subject.create_document(structure_shape.type.new(string: 'hello', union: { string: 'world' })) + expect(subject.serialize_document(document, use_json_name: true)).to include( + '__type' => 'smithy.ruby.tests#Structure', + 'A' => 'hello', + 'union' => { 'B' => 'world' } + ) + end + + it 'returns serialized data with timestampTrait when applicable' do + shapes['smithy.ruby.tests#Structure']['members']['timestampDateTime'] = { + 'target' => 'smithy.api#Timestamp', + 'traits' => { 'smithy.api#timestampFormat' => 'date-time' } + } + shapes['smithy.ruby.tests#Structure']['members']['timestampHttpDate'] = { + 'target' => 'smithy.api#Timestamp', + 'traits' => { 'smithy.api#timestampFormat' => 'http-date' } + } + shapes['smithy.ruby.tests#Structure']['members']['timestampEpochSeconds'] = { + 'target' => 'smithy.api#Timestamp', + 'traits' => { 'smithy.api#timestampFormat' => 'epoch-seconds' } + } + shapes['smithy.ruby.tests#Structure']['members']['timestampUseShape'] = { + 'target' => 'smithy.ruby.tests#TimestampUseShape' + } + shapes['smithy.ruby.tests#TimestampUseShape'] = { + 'type' => 'timestamp', + 'traits' => { 'smithy.api#timestampFormat' => 'http-date' } } - JSON - let(:subject) { Document.new(JSON.parse(json)) } + struct = structure_shape.type.new( + timestamp_date_time: Time.utc(2024, 12, 25), + timestamp_http_date: Time.utc(2024, 12, 25), + timestamp_epoch_seconds: Time.utc(2024, 12, 25), + timestamp_use_shape: Time.utc(2024, 12, 25) + ) - describe '#initialize' do - it 'sets discriminator' do - expect(subject.discriminator).to eq('foo.example#string') + document = subject.create_document(struct) + expect(subject.serialize_document(document, use_timestamp_format: true)).to include( + '__type' => 'smithy.ruby.tests#Structure', + 'timestampDateTime' => '2024-12-25T00:00:00Z', + 'timestampHttpDate' => 'Wed, 25 Dec 2024 00:00:00 GMT', + 'timestampEpochSeconds' => 1_735_084_800, + 'timestampUseShape' => 'Wed, 25 Dec 2024 00:00:00 GMT' + ) + end + + it 'raises when an invalid document is given' do + expect do + subject.serialize_document('foo') + end.to raise_error(ArgumentError) + expect do + subject.serialize_document(untyped_document) + end.to raise_error(ArgumentError) + end + + it 'raises when discriminator cannot be found in type registry' do + # TODO + end + end + end + + describe Deserializer do + it 'TBD' do + # TODO + end + end + + context 'SERDE test cases' do # don't look ... its not ready >_< + tests = JSON.load_file(File.expand_path('../fixtures/typed-documents/test-cases.json', __dir__.to_s)) + let(:test_model) { JSON.load_file(File.expand_path('../fixtures/typed-documents/model.json', __dir__.to_s)) } + let(:schema) { SchemaHelper.sample_schema(model: test_model) } + let(:structure_shape) { schema.const_get(:OmniWidget) } + let(:type_registry) { schema.const_get(:TYPE_REGISTRY) } + + # def create_runtime_shape(data, shape) + # Document.new(data, shape: shape).as_typed(shape) + # end + + # def document_options(settings) + # settings.each_with_object({}) do |(k, v), o| + # case k + # when 'jsonName' + # o[:disable_json_name] = true if v == false + # when 'timestampFormat' + # o[:disable_timestamp_format] = true if v['useTrait'] == false + # end + # end + # end + + tests['serdeTests'].each do |test_case| + context "Case: #{test_case['name']}" do + # let(:data_object) { create_runtime_shape(test_case['deserialized'], structure_shape) } + # let(:document_object) { Document.new(test_case['serialized'], shape: structure_shape) } + # let(:serialized_data) { test_case['serialized'] } + # let(:settings) { document_options(test_case['settings']) } + + it 'when data object is converted to a Document, it deeply equals the document object' do + # document = Document.new(data_object, shape: structure_shape) + # expect(document.data).to eq(document_object.data) end - it 'data does not include a discriminator' do - expect(subject.data).not_to include('__type') + it 'when document object is deserialized, it deeply equals data object' do + # expect(document_object.as_typed(structure_shape).to_h).to eq(data_object.to_h) + end + + it 'when data object is serialized, it equals serialized data' do + # document = Document.new(data_object, shape: structure_shape) + # expect(document.as_json(structure_shape, settings)).to eq(serialized_data.except('__type')) + end + + it 'when serialized data is deserialized, it equals data object' do + # document = Document.new(serialized_data, shape: structure_shape) + # expect(document.as_typed(structure_shape).to_h).to eq(data_object.to_h) + end + + it 'when document object is serialized, it equals serialized data' do + # serialized_document = document_object.as_json(structure_shape, settings) + # expect(serialized_document).to eq(serialized_data.except('__type')) end - end - describe '#as_typed' do - it 'converts document as a runtime shape' do - typed_shape = subject.as_typed(simple_schema) - expect(typed_shape).to be_a(simple_runtime) + # Assert that the serialized test data (3) can be parsed into a document (2) + it 'serialized data can be parsed into document' do + # expect(document_object).to be_an_instance_of(Document) end end end diff --git a/gems/smithy/lib/smithy/views/client/schema.rb b/gems/smithy/lib/smithy/views/client/schema.rb index f1ad63feb..13c5d5594 100644 --- a/gems/smithy/lib/smithy/views/client/schema.rb +++ b/gems/smithy/lib/smithy/views/client/schema.rb @@ -159,8 +159,7 @@ def initialize(service, id, shape) @traits = shape['traits'] || {} end - attr_reader :type - attr_reader :id + attr_reader :type, :id def name @service.dig('rename', @id) || Model::Shape.name(@id).camelize From 9cef7eeb513b35f720121a63b8beb5d05da7470f Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Wed, 7 May 2025 08:28:51 -0700 Subject: [PATCH 43/54] Rename document test cases --- .../spec/fixtures/{typed-documents => documents}/model.json | 0 .../spec/fixtures/{typed-documents => documents}/test-cases.json | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename gems/smithy-schema/spec/fixtures/{typed-documents => documents}/model.json (100%) rename gems/smithy-schema/spec/fixtures/{typed-documents => documents}/test-cases.json (100%) diff --git a/gems/smithy-schema/spec/fixtures/typed-documents/model.json b/gems/smithy-schema/spec/fixtures/documents/model.json similarity index 100% rename from gems/smithy-schema/spec/fixtures/typed-documents/model.json rename to gems/smithy-schema/spec/fixtures/documents/model.json diff --git a/gems/smithy-schema/spec/fixtures/typed-documents/test-cases.json b/gems/smithy-schema/spec/fixtures/documents/test-cases.json similarity index 100% rename from gems/smithy-schema/spec/fixtures/typed-documents/test-cases.json rename to gems/smithy-schema/spec/fixtures/documents/test-cases.json From e283281f6f6edfc62c9ad347056929a6bc01db12 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Wed, 7 May 2025 08:51:18 -0700 Subject: [PATCH 44/54] Improve Document Deserializer --- .../smithy-schema/document/deserializer.rb | 174 +++++++++++++++--- .../document/deserializer_spec.rb | 100 ++++++++++ 2 files changed, 247 insertions(+), 27 deletions(-) create mode 100644 gems/smithy-schema/spec/smithy-schema/document/deserializer_spec.rb diff --git a/gems/smithy-schema/lib/smithy-schema/document/deserializer.rb b/gems/smithy-schema/lib/smithy-schema/document/deserializer.rb index 3b1ef9c14..aef1cda3b 100644 --- a/gems/smithy-schema/lib/smithy-schema/document/deserializer.rb +++ b/gems/smithy-schema/lib/smithy-schema/document/deserializer.rb @@ -3,59 +3,179 @@ module Smithy module Schema module Document - # TODO + # Deserializes document data into runtime shape. class Deserializer + include Shapes + + # @param [TypeRegistry] type_registry required to find shape based + # on document discriminator. def initialize(type_registry) @type_registry = type_registry end - # TODO......... + # Deserializes a {Data} into a runtime shape. + # + # @param [Data] document The document to deserialize. Must have + # a discriminator that maps to a shape in the type registry. + # @param [StructureShape, nil] shape Optional shape to use for + # deserialization. If provided, this shape takes precedence over the + # document's discriminator. The shape must have a type. + # @return [Object] deserialized runtime shape + # + # @example Standard Example + # # create deserializer with an existing type registry + # deserializer = Smithy::Schema::Document::Deserializer(type_registry) + # + # deserializer.deserialize(document) # passing document data + # # => # + # + # @example Providing a shape as input + # # using the existing discriminator above + # # given shape is a structure and has a type + # deserializer.deserialize(document, shape: some_structure) + # # => # def deserialize(document, shape: nil) - # TODO + validate_input(document, shape) + + shape ||= resolve_shape(document) + shape(ShapeRef.new(shape: shape), document.data, shape.type.new) end private - def valid_shape?(shape) - shape.is_a?(Shapes::StructureShape) && !shape.type.nil? + def validate_input(document, shape) + msg = 'document must be an instance of `Document::Data` class' + raise ArgumentError, msg unless document.is_a?(Data) + + if shape + msg = 'invalid shape - must be a structure shape with type' + raise ArgumentError, msg unless valid_shape(shape) + else + msg = 'invalid document - must have a discriminator' + raise ArgumentError, msg unless document.discriminator + end + end + + def valid_shape(shape) + shape.is_a?(StructureShape) && shape.type + end + + def resolve_shape(document) + msg = 'invalid document - document discriminator not found in type registry' + raise ArgumentError, msg unless @type_registry.key?(document.discriminator) + + @type_registry[document.discriminator] end - # Apply data into a given runtime shape - def parse(ref, value, type) # rubocop:disable Metrics/CyclomaticComplexity + def shape(ref, value, target = nil) # rubocop:disable Metrics/CyclomaticComplexity case ref.shape - when Shapes::StructureShape then structure(ref, value, type) - when Shapes::UnionShape then union(ref, value, type) - when Shapes::ListShape then list(ref, value, type) - when Shapes::MapShape then map(ref, value, type) - when Shapes::TimestampShape then timestamp(ref, value, type) - when Shapes::DocumentShape then document(ref, value, type) - when Shapes::BlobShape then Base64.strict_decode64(value) - else data + when StructureShape then structure(ref, value, target) + when UnionShape then union(ref, value, target) + when ListShape then list(ref, value, target) + when MapShape then map(ref, value, target) + when TimestampShape then timestamp(value) + when DocumentShape then document(value) + when BlobShape then Base64.strict_decode64(value) + when FloatShape then float(value) + else value end end - def document(ref, value, type = nil) - # TODO + def document(values) + return values unless values.is_a?(Hash) && values.key?('__type') + + msg = 'invalid document - document discriminator not found in type registry' + raise ArgumentError, msg unless @type_registry.key? + + shape_ref = ShapeRef.new(shape: @type_registry[values['__type']]) + shape(shape_ref, values) end - def list(ref, value, type = nil) - # TODO + def float(value) + case value + when 'Infinity' then ::Float::INFINITY + when '-Infinity' then -::Float::INFINITY + when 'NaN' then ::Float::NAN + when nil then nil + else value.to_f + end end - def map(ref, values, type = nil) - # TODO + def list(ref, values, target = nil) + target = [] if target.nil? + values.each do |value| + next if value.nil? && !sparse?(ref.shape) + + target << (value.nil? ? nil : shape(ref.shape.member, value)) + end + target end - def structure(ref, values, type = nil) - # TODO + def map(ref, values, target = nil) + target = {} if target.nil? + values.each do |key, value| + next if value.nil? && !sparse?(ref.shape) + + target[key] = value.nil? ? nil : shape(ref.shape.value, value) + end + target end - def timestamp(ref, values, type = nil) - # TODO + def structure(ref, values, target = nil) + return Smithy::Schema::EmptyStructure.new if ref.shape == Prelude::Unit + + target = ref.shape.type.new if target.nil? + ref.shape.members.each do |member_name, member_ref| + name = member_ref.member_name + next unless values.key?(name) + + target[member_name] = shape(member_ref, values[name]) + end + target + 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 + end + end + + def union(ref, values, target = nil) # rubocop:disable Metrics/AbcSize + validate_union!(values) + + key, value = values.first + return if key.nil? + + ref.shape.members.each do |member_name, member_ref| + name = member_ref.member_name + next unless values.key?(name) + + target = ref.shape.member_type(member_name) if target.nil? + return target.new(shape(member_ref, values[name])) + end + ref.shape.member_type(:unknown).new(key, value) + end + + def validate_union!(values) + return unless values.size > 1 + + msg = "union value includes more than one key, received: #{values.keys}" + raise ArgumentError, msg if values.size > 1 end - def union(ref, values, type = nil) - # TODO + def sparse?(shape) + shape.traits.include?('smithy.api#sparse') end end end diff --git a/gems/smithy-schema/spec/smithy-schema/document/deserializer_spec.rb b/gems/smithy-schema/spec/smithy-schema/document/deserializer_spec.rb new file mode 100644 index 000000000..179166a7c --- /dev/null +++ b/gems/smithy-schema/spec/smithy-schema/document/deserializer_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require_relative '../../spec_helper' +require_relative '../../support/schema_helper' + +module Smithy + module Schema + module Document + describe Deserializer do + let(:shapes) do + shapes = SchemaHelper.sample_shapes + shapes['smithy.ruby.tests#Foo'] = shapes['smithy.ruby.tests#Structure'] + shapes['smithy.ruby.tests#Operation']['output']['target'] = 'smithy.ruby.tests#Foo' + shapes + end + + let(:sample_schema) { SchemaHelper.sample_schema(shapes: shapes) } + let(:type_registry) { sample_schema.const_get(:TYPE_REGISTRY) } + let(:structure_shape) { sample_schema.const_get(:Structure) } + let(:another_shape) { sample_schema.const_get(:Foo) } + let(:typed_shape) do + structure_shape.type.new( + big_decimal: 0, + big_integer: 0, + blob: StringIO.new('foo'), + boolean: true, + byte: 1, + document: true, + double: 1.1, + float: 1.1, + enum: 'enum', + int_enum: 0, + integer: 1, + long: 1, + list: %w[Item1 Item2], + map: { color: 'red' }, + short: 1, + streaming_blob: 'streaming blob', + string: 'foo', + structure_list: [{ integer: 1 }, { integer: 2 }, { integer: 3 }], + structure_map: { 'key' => { map: { 'color' => 'blue' } } }, + timestamp: 1_735_084_800, + union: { string: 'string' } + ) + end + + subject { Deserializer.new(type_registry) } + let(:typed_document) { Serializer.new(type_registry).create_document(typed_shape) } + + describe '#deserialize' do + it 'deserializes document into correct runtime shape using discriminator' do + runtime_shape = subject.deserialize(typed_document) + expect(runtime_shape).to be_a_kind_of(Structure) + expect(runtime_shape).to be_an_instance_of(structure_shape.type) + expect(runtime_shape.to_h).to eq( + big_decimal: 0, + big_integer: 0, + blob: 'foo', + boolean: true, + byte: 1, + double: 1.1, + enum: 'enum', + float: 1.1, + int_enum: 0, + integer: 1, + list: %w[Item1 Item2], + long: 1, + map: { 'color' => 'red' }, + document: true, + short: 1, + streaming_blob: 'streaming blob', + structure_list: [{ integer: 1 }, { integer: 2 }, { integer: 3 }], + structure_map: { 'key' => { map: { 'color' => 'blue' } } }, + string: 'foo', + timestamp: Time.at(1_735_084_800).utc, + union: { string: 'string' } + ) + end + + it 'prioritizes provided shape over document discriminator when deserializing' do + runtime_shape = subject.deserialize(typed_document, shape: another_shape) + expect(runtime_shape).to be_a_kind_of(Structure) + expect(runtime_shape).to be_an_instance_of(another_shape.type) + end + + it 'raises when given invalid inputs' do + expect { subject.deserialize('foo') }.to raise_error(ArgumentError) + expect { subject.deserialize(Data.new({})) }.to raise_error(ArgumentError) + expect do + subject.deserialize(Data.new({}, discriminator: 'InvalidShape')) + end.to raise_error(ArgumentError) + expect do + subject.deserialize(typed_document, shape: Shapes::StringShape.new) + end.to raise_error(ArgumentError) + end + end + end + end + end +end From a48f7a1908dcd4ccbd53060a4bbed81af12be847 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Wed, 7 May 2025 13:13:12 -0700 Subject: [PATCH 45/54] Update Document Serializer and its specs --- .../smithy-schema/document/deserializer.rb | 3 +- .../lib/smithy-schema/document/serializer.rb | 281 +++++++++++------- .../smithy-schema/document/serializer_spec.rb | 208 +++++++++++++ 3 files changed, 379 insertions(+), 113 deletions(-) create mode 100644 gems/smithy-schema/spec/smithy-schema/document/serializer_spec.rb diff --git a/gems/smithy-schema/lib/smithy-schema/document/deserializer.rb b/gems/smithy-schema/lib/smithy-schema/document/deserializer.rb index aef1cda3b..ff4a99e65 100644 --- a/gems/smithy-schema/lib/smithy-schema/document/deserializer.rb +++ b/gems/smithy-schema/lib/smithy-schema/document/deserializer.rb @@ -28,7 +28,6 @@ def initialize(type_registry) # # deserializer.deserialize(document) # passing document data # # => # - # # @example Providing a shape as input # # using the existing discriminator above # # given shape is a structure and has a type @@ -61,7 +60,7 @@ def valid_shape(shape) end def resolve_shape(document) - msg = 'invalid document - document discriminator not found in type registry' + msg = 'document discriminator not found in type registry' raise ArgumentError, msg unless @type_registry.key?(document.discriminator) @type_registry[document.discriminator] diff --git a/gems/smithy-schema/lib/smithy-schema/document/serializer.rb b/gems/smithy-schema/lib/smithy-schema/document/serializer.rb index a63848160..0e9e7680e 100644 --- a/gems/smithy-schema/lib/smithy-schema/document/serializer.rb +++ b/gems/smithy-schema/lib/smithy-schema/document/serializer.rb @@ -6,67 +6,88 @@ module Smithy module Schema module Document - # TODO + # Serializes data into a document data. class Serializer + include Shapes + + # @param [TypeRegistry] type_registry Used to find shape based on + # on document discriminator. def initialize(type_registry) @type_registry = type_registry end - # data can come in several forms - # - ruby objects w/o attachments with schema - # - instance of runtime shape (requires shape to properly serialize) - # - json response that includes discriminator (requires shape to properly serialize) + # Create document data from various input data formats + # @param [Object] data Input data can be: Ruby objects, instance of a runtime shape or a + # JSON response with type discriminator. + # @return [Data] document data + # + # @example Ruby objects as input + # # create serializer with an existing type registry + # serializer = Smithy::Schema::Document::Serializer(type_registry) + # + # # ruby objects as input + # serializer.create_document("some document") + # # => {"foo" => "bar"} + # @example Runtime shape as input + # # create serializer with an existing type registry + # serializer = Smithy::Schema::Document::Serializer(type_registry) + # + # # given the following runtime shape + # runtime_shape = some_structure.new.type(some_data) + # # => # + # + # serializer.create_document(runtime_shape) + # # => an instance of Smithy::Schema::Document::Data + # @example JSON data + # # create serializer with an existing type registry + # serializer = Smithy::Schema::Document::Serializer(type_registry) + # + # # given the following json data + # parsed_json = { + # "__type" => "smithy.ruby.tests#Structure", + # "string" => "hello" + # } + # + # document = serializer.create_document(parsed_json) + # # => an instance of Smithy::Schema::Document::Data + # document.discriminator + # # => "smithy.ruby.tests#Structure" def create_document(data) - raise ArgumentError, 'Unable to create Document' if data.nil? + validate_data(data) case data - when Smithy::Schema::Structure # runtime shape - msg = 'Given runtime shape not found in type registry' - raise ArgumentError, msg unless @type_registry.shape_by_type?(data.class) - + when Smithy::Schema::Structure shape = @type_registry.shape_by_type(data.class) - ref = Smithy::Schema::Shapes::ShapeRef.new(shape: shape) - new_data = build(ref, data) - new_data['__type'] = shape.id - Data.new(new_data, discriminator: shape.id) + Data.new(format_document_data(shape, data), discriminator: shape.id) else - # shape is typed if discriminator?(data) - msg = 'Given discriminator not found in type registry' - raise ArgumentError, msg unless @type_registry.key?(data['__type']) - - discriminator = data['__type'] - shape = @type_registry[discriminator] - ref = Smithy::Schema::Shapes::ShapeRef.new(shape: shape) - new_data = build(ref, data) - new_data['__type'] = shape.id - Data.new(new_data, discriminator: shape.id) + shape = @type_registry[data['__type']] + Data.new(format_document_data(shape, data), discriminator: shape.id) else - Data.new(build_untyped(data)) + Data.new(serialize_untyped(data)) end end end + # Serializes a document data with optional formatting. + # @param [Data] document The document to serialize + # @param [Hash] opts serialization options + # @option opts [Boolean] :use_timestamp_format Whether to use the + # `timestampFormat` trait or ignore it. The `timestampFormat` trait + # is ignored by default. + # @option opts [Boolean] :use_json_name Whether to use `jsonName` trait + # or just member name. The `jsonName` trait is ignored by default. + # @return [Hash] Serialized document data def serialize_document(document, opts = {}) - error_message = 'Invalid Document - must be a typed document' - raise ArgumentError, error_message unless document.is_a?(Data) && document.discriminator + validate_document(document) opts[:discriminator] = true - shape = @type_registry[document.discriminator] - ref = Smithy::Schema::Shapes::ShapeRef.new(shape: shape) - new_data = build(ref, document.data, opts) - new_data['__type'] = shape.id - new_data + format_document_data(resolve_shape(document), document.data, opts) end private - def discriminator?(data) - data.is_a?(Hash) && data.key?('__type') - end - - # Takes untyped ruby data into document-friendly format - def build_untyped(values) + def serialize_untyped(values) return if values.nil? case values @@ -74,85 +95,65 @@ def build_untyped(values) values.to_i # timestamp format is "epoch-seconds" by default when Hash values.each_with_object({}) do |(k, v), h| - h[k.to_s] = build_untyped(v) + h[k.to_s] = serialize_untyped(v) end when Array - values.map { |d| build_untyped(d) } + values.map { |d| serialize_untyped(d) } else values end end - # Construct data into a document-friendly format using a shape - def build(ref, values, opts = {}) # rubocop:disable Metrics/CyclomaticComplexity + def shape(ref, values, opts = {}) # rubocop:disable Metrics/CyclomaticComplexity return if values.nil? case ref.shape - when Shapes::StructureShape then structure(ref, values, opts) - when Shapes::UnionShape then union(ref, values, opts) - when Shapes::ListShape then list(ref, values, opts) - when Shapes::MapShape then map(ref, values, opts) - when Shapes::BlobShape then blob(values, opts) - when Shapes::FloatShape then float(values, opts) - when Shapes::TimestampShape then timestamp(ref, values, opts) - when Shapes::DocumentShape then document(values, opts) + when BlobShape then blob(values, opts) + when DocumentShape then document(values, opts) + when FloatShape then float(values, opts) + when ListShape then list(ref, values, opts) + when MapShape then map(ref, values, opts) + when StructureShape then structure(ref, values, opts) + when TimestampShape then timestamp(ref, values, opts) + when UnionShape then union(ref, values, opts) else values end end - def structure(ref, values, opts) - values.each_pair.with_object({}) do |(k, v), h| - next if v.nil? - - member_ref = ref.shape.member(k) || member_by_location_name(ref.shape, k) - next if member_ref.nil? + def blob(value, opts) + return value if opts[:discriminator] # blob is already encoded - member_name = - if opts[:use_json_name] && member_ref.traits['smithy.api#jsonName'] - member_ref.traits['smithy.api#jsonName'] - else - member_ref.location_name - end - h[member_name] = build(member_ref, v, opts) - end + Base64.strict_encode64(value.is_a?(String) ? value : value.read) end - def member_by_location_name(shape, name) - shape.members.values.find { |ref| ref.location_name == name } + def document(values, opts) + if values.is_a?(Shapes::Structure) + shape = @type_registry.shape_by_type(values.class) + data = shape(ShapeRef.new(shape: shape), values, opts) + data['__type'] = shape.id + data + else + values + end end - def union(ref, values, opts) - data = {} - if values.is_a?(Smithy::Schema::Union) - member_ref = ref.shape.member_by_type(values.class) - member_name = - if opts[:use_json_name] && member_ref.traits['smithy.api#jsonName'] - member_ref.traits['smithy.api#jsonName'] - else - member_ref.location_name - end - data[member_name] = build(member_ref, values, opts) + def float(value, _opts) + if value == ::Float::INFINITY + 'Infinity' + elsif value == -::Float::INFINITY + '-Infinity' + elsif value.nan? + 'NaN' else - key, value = values.first - member_ref = ref.shape.member(key) || member_by_location_name(ref.shape, key) - unless member_ref.nil? - member_name = - if opts[:use_json_name] && member_ref.traits['smithy.api#jsonName'] - member_ref.traits['smithy.api#jsonName'] - else - member_ref.location_name - end - data[member_name] = build(member_ref, value, opts) - end + value end - data end def list(ref, values, opts) values.collect do |value| next if value.nil? - build(ref.shape.member, value, opts) + shape(ref.shape.member, value, opts) end end @@ -160,21 +161,26 @@ def map(ref, values, opts) values.each.with_object({}) do |(key, value), data| next if value.nil? - data[key.to_s] = build(ref.shape.value, value, opts) + data[key.to_s] = shape(ref.shape.value, value, opts) end end - def blob(value, opts) - return value if opts[:discriminator] # blob is already encoded + def structure(ref, values, opts) + values.each_pair.with_object({}) do |(k, v), h| + next if v.nil? - Base64.strict_encode64(value.is_a?(String) ? value : value.read) + if (member_ref = resolve_member_ref(ref, k)) + member_name = resolve_member_name(member_ref, opts) + h[member_name] = shape(member_ref, v, opts) + end + end end def timestamp(ref, value, opts) return value.to_i unless opts[:use_timestamp_format] trait = 'smithy.api#timestampFormat' - value = value.is_a?(Numeric) ? Time.at(value) : Time.parse(value) unless value.is_a?(Time) + value = normalize_timestamp_value(value) case ref.traits[trait] || ref.shape.traits[trait] when 'date-time' then value.utc.iso8601 when 'http-date' then value.utc.httpdate @@ -184,29 +190,82 @@ def timestamp(ref, value, opts) end end - def float(value, _opts) - if value == ::Float::INFINITY - 'Infinity' - elsif value == -::Float::INFINITY - '-Infinity' - elsif value.nan? - 'NaN' + def union(ref, values, opts) + data = {} + if values.is_a?(Smithy::Schema::Union) + member_ref = ref.shape.member_by_type(values.class) + member_name = resolve_member_name(member_ref, opts) + data[member_name] = shape(member_ref, values, opts) else - value + key, value = values.first + if (member_ref = resolve_member_ref(ref, key)) + member_name = resolve_member_name(member_ref, opts) + data[member_name] = shape(member_ref, value, opts) + end end + data end - def document(values, opts) - if values.is_a?(Smithy::Schema::Structure) - shape = @type_registry.shape_by_type(values.class) - shape_ref = Smithy::Schema::Shapes::ShapeRef.new(shape: shape) - data = build(shape_ref, values, opts) - data['__type'] = shape.id - data + def format_document_data(shape, data, opts = {}) + ref = ShapeRef.new(shape: shape) + document_data = shape(ref, data, opts) + document_data['__type'] = shape.id + document_data + end + + def discriminator?(data) + data.is_a?(Hash) && data.key?('__type') + end + + def normalize_timestamp_value(value) + case value + when Time then value + when Numeric then Time.at(value) + else Time.parse(value) + end + end + + def resolve_member_ref(ref, name) + ref.shape.member(name) || + (ref.shape.members.values.find { |r| r.member_name == name }) + end + + def resolve_member_name(member_ref, opts) + json_trait = 'smithy.api#jsonName' + if opts[:use_json_name] && member_ref.traits[json_trait] + member_ref.traits[json_trait] else - values + member_ref.member_name + end + end + + def resolve_shape(document) + msg = 'document discriminator not found in type registry' + raise ArgumentError, msg unless @type_registry.key?(document.discriminator) + + @type_registry[document.discriminator] + end + + def validate_data(data) + raise ArgumentError, 'invalid data - data cannot be nil' if data.nil? + + case data + when Schema::Structure + msg = 'given runtime shape not found in type registry' + raise ArgumentError, msg unless @type_registry.shape_by_type?(data.class) + else + msg = 'document discriminator not found in type registry' + raise ArgumentError, msg if discriminator?(data) && !@type_registry.key?(data['__type']) end end + + def validate_document(document) + msg = 'document must be an instance of `Document::Data` class' + raise ArgumentError, msg unless document.is_a?(Data) + + msg = 'invalid document - must have a discriminator' + raise ArgumentError, msg unless document.discriminator + end end end end diff --git a/gems/smithy-schema/spec/smithy-schema/document/serializer_spec.rb b/gems/smithy-schema/spec/smithy-schema/document/serializer_spec.rb new file mode 100644 index 000000000..de2dca0cb --- /dev/null +++ b/gems/smithy-schema/spec/smithy-schema/document/serializer_spec.rb @@ -0,0 +1,208 @@ +# frozen_string_literal: true + +require_relative '../../spec_helper' +require_relative '../../support/schema_helper' + +module Smithy + module Schema + module Document + describe Serializer do + let(:shapes) do + shapes = SchemaHelper.sample_shapes + shapes['smithy.ruby.tests#Structure']['members']['timestampDateTime'] = { + 'target' => 'smithy.api#Timestamp', 'traits' => { 'smithy.api#timestampFormat' => 'date-time' } + } + shapes['smithy.ruby.tests#Structure']['members']['timestampHttpDate'] = { + 'target' => 'smithy.api#Timestamp', 'traits' => { 'smithy.api#timestampFormat' => 'http-date' } + } + shapes['smithy.ruby.tests#Structure']['members']['timestampEpochSeconds'] = { + 'target' => 'smithy.api#Timestamp', 'traits' => { 'smithy.api#timestampFormat' => 'epoch-seconds' } + } + shapes['smithy.ruby.tests#Structure']['members']['timestampUseShape'] = { + 'target' => 'smithy.ruby.tests#TimestampUseShape' + } + shapes['smithy.ruby.tests#TimestampUseShape'] = { + 'type' => 'timestamp', 'traits' => { 'smithy.api#timestampFormat' => 'http-date' } + } + shapes['smithy.ruby.tests#Structure']['members']['string']['traits'] = { 'smithy.api#jsonName' => 'A' } + shapes['smithy.ruby.tests#Union']['members']['string']['traits'] = { 'smithy.api#jsonName' => 'B' } + shapes + end + let(:sample_schema) { SchemaHelper.sample_schema(shapes: shapes) } + let(:type_registry) { sample_schema.const_get(:TYPE_REGISTRY) } + let(:structure_shape) { sample_schema.const_get(:Structure) } + let(:typed_shape) do + structure_shape.type.new( + big_decimal: 0, + big_integer: 0, + blob: StringIO.new('foo'), + boolean: true, + byte: 1, + document: true, + double: 1.1, + float: 1.1, + enum: 'enum', + int_enum: 0, + integer: 1, + long: 1, + short: 1, + list: %w[Item1 Item2], + map: { color: 'red' }, + streaming_blob: 'streaming blob', + string: 'foo', + structure_list: [{ integer: 1 }, { integer: 2 }, { integer: 3 }], + structure_map: { 'key' => { map: { 'color' => 'blue' } } }, + timestamp: Time.utc(2024, 12, 25), + union: { string: 'string' } + ) + end + let(:expected_typed_data) do + { + '__type' => 'smithy.ruby.tests#Structure', + 'bigDecimal' => 0, + 'bigInteger' => 0, + 'blob' => 'Zm9v', + 'boolean' => true, + 'byte' => 1, + 'document' => true, + 'double' => 1.1, + 'float' => 1.1, + 'enum' => 'enum', + 'intEnum' => 0, + 'integer' => 1, + 'long' => 1, + 'short' => 1, + 'list' => %w[Item1 Item2], + 'map' => { 'color' => 'red' }, + 'streamingBlob' => 'c3RyZWFtaW5nIGJsb2I=', + 'string' => 'foo', + 'structureList' => [{ 'integer' => 1 }, { 'integer' => 2 }, { 'integer' => 3 }], + 'structureMap' => { 'key' => { 'map' => { 'color' => 'blue' } } }, + 'timestamp' => 1_735_084_800, + 'union' => { 'string' => 'string' } + } + end + + subject { Document::Serializer.new(type_registry) } + let(:typed_document) { subject.create_document(typed_shape) } + let(:untyped_document) { subject.create_document(foo: 'bar') } + + describe '#create_document' do + context 'with untyped data' do + it 'returns a document data' do + expect(untyped_document).to be_a_kind_of(Data) + end + + it 'sets data' do + expect(untyped_document.data).to eq('foo' => 'bar') + end + + it 'sets discriminator to nil' do + expect(untyped_document.discriminator).to be_nil + end + + it 'sets time data using default timestamp format' do + doc = subject.create_document(Time.utc(2024, 12, 25)) + expect(doc.data).to eq(1_735_084_800) + end + + it 'raises when given invalid data' do + expect do + subject.create_document(nil) + end.to raise_error(ArgumentError) + end + end + + context 'with runtime shape' do + let(:invalid_runtime) do + Struct.new(:string, keyword_init: true) do + include Smithy::Schema::Structure + end + end + + it 'sets data' do + expect(typed_document.data).to include(expected_typed_data) + end + + it 'sets discriminator' do + expect(typed_document.discriminator).to eql(structure_shape.id) + end + + it 'raises when runtime shape not found in type registry' do + expect do + subject.create_document(invalid_runtime.new(string: 'foo')) + end.to raise_error(ArgumentError) + end + end + + context 'with parsed JSON input' do + let(:json) do + { + '__type' => 'smithy.ruby.tests#Structure', + 'string' => 'hello' + } + end + let(:document) { subject.create_document(json) } + + it 'sets data' do + expect(document.data).to include(json) + end + + it 'sets discriminator' do + expect(document.discriminator).to eql(structure_shape.id) + end + + it 'raises when discriminator not found in type registry' do + json['__type'] = 'smithy.ruby.tests#Invalid' + expect do + subject.create_document(json) + end.to raise_error(ArgumentError) + end + end + end + + describe '#serialize_document' do + it 'returns serialized data' do + expect(subject.serialize_document(typed_document)) + .to include(expected_typed_data) + end + + it 'applies jsonName trait to serialized data when configured' do + document = subject.create_document(structure_shape.type.new(string: 'hello', union: { string: 'world' })) + expect(subject.serialize_document(document, use_json_name: true)).to include( + '__type' => 'smithy.ruby.tests#Structure', + 'A' => 'hello', + 'union' => { 'B' => 'world' } + ) + end + + it 'applies timestampFormat trait to serialized data when configured' do + struct = structure_shape.type.new( + timestamp_date_time: Time.utc(2024, 12, 25), + timestamp_http_date: Time.utc(2024, 12, 25), + timestamp_epoch_seconds: Time.utc(2024, 12, 25), + timestamp_use_shape: Time.utc(2024, 12, 25) + ) + document = subject.create_document(struct) + expect(subject.serialize_document(document, use_timestamp_format: true)).to include( + '__type' => 'smithy.ruby.tests#Structure', + 'timestampDateTime' => '2024-12-25T00:00:00Z', + 'timestampHttpDate' => 'Wed, 25 Dec 2024 00:00:00 GMT', + 'timestampEpochSeconds' => 1_735_084_800, + 'timestampUseShape' => 'Wed, 25 Dec 2024 00:00:00 GMT' + ) + end + + it 'raises when an invalid document is given' do + expect do + subject.serialize_document('foo') + end.to raise_error(ArgumentError) + expect do + subject.serialize_document(untyped_document) + end.to raise_error(ArgumentError) + end + end + end + end + end +end From 191393834f34bb7f58e28a546b8d974a0da42fb1 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Wed, 7 May 2025 13:13:50 -0700 Subject: [PATCH 46/54] Update Document Data class --- .../lib/smithy-schema/document.rb | 4 +- .../spec/smithy-schema/document_spec.rb | 217 +----------------- 2 files changed, 4 insertions(+), 217 deletions(-) diff --git a/gems/smithy-schema/lib/smithy-schema/document.rb b/gems/smithy-schema/lib/smithy-schema/document.rb index b59206074..5a7a551dc 100644 --- a/gems/smithy-schema/lib/smithy-schema/document.rb +++ b/gems/smithy-schema/lib/smithy-schema/document.rb @@ -13,14 +13,14 @@ module Document # # Smithy-Ruby currently only support JSON documents. class Data < ::SimpleDelegator - # @param [Object] data document data + # @param [Object] data document data # @param [Hash] options # @option options [Smithy::Schema::StructureShape] :shape shape to reference when setting # document data. def initialize(data, options = {}) @data = data - @discriminator = options[:discriminator] || nil super(@data) + @discriminator = options[:discriminator] || nil end # @return [String] discriminator diff --git a/gems/smithy-schema/spec/smithy-schema/document_spec.rb b/gems/smithy-schema/spec/smithy-schema/document_spec.rb index 7fe10965e..c9239be8a 100644 --- a/gems/smithy-schema/spec/smithy-schema/document_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/document_spec.rb @@ -36,222 +36,9 @@ module Document end end - describe Serializer do - let(:shapes) { SchemaHelper.sample_shapes } - let(:sample_schema) { SchemaHelper.sample_schema(shapes: shapes) } - let(:type_registry) { sample_schema.const_get(:TYPE_REGISTRY) } - let(:structure_shape) { sample_schema.const_get(:Structure) } - let(:typed_shape) do - structure_shape.type.new( - big_decimal: 0, - big_integer: 0, - blob: StringIO.new('foo'), - boolean: true, - byte: 1, - document: true, - double: 1.1, - float: 1.1, - enum: 'enum', - int_enum: 0, - integer: 1, - long: 1, - short: 1, - list: %w[Item1 Item2], - map: { color: 'red' }, - streaming_blob: 'streaming blob', - string: 'foo', - structure_list: [{ integer: 1 }, { integer: 2 }, { integer: 3 }], - structure_map: { 'key' => { map: { 'color' => 'blue' } } }, - timestamp: Time.utc(2024, 12, 25), - union: { string: 'string' } - ) - end - - let(:expected_typed_data) do - { - '__type' => 'smithy.ruby.tests#Structure', - 'bigDecimal' => 0, - 'bigInteger' => 0, - 'blob' => 'Zm9v', - 'boolean' => true, - 'byte' => 1, - 'document' => true, - 'double' => 1.1, - 'float' => 1.1, - 'enum' => 'enum', - 'intEnum' => 0, - 'integer' => 1, - 'long' => 1, - 'short' => 1, - 'list' => %w[Item1 Item2], - 'map' => { 'color' => 'red' }, - 'streamingBlob' => 'c3RyZWFtaW5nIGJsb2I=', - 'string' => 'foo', - 'structureList' => [{ 'integer' => 1 }, { 'integer' => 2 }, { 'integer' => 3 }], - 'structureMap' => { 'key' => { 'map' => { 'color' => 'blue' } } }, - 'timestamp' => 1_735_084_800, - 'union' => { 'string' => 'string' } - } - end - - subject { Document::Serializer.new(type_registry) } - let(:typed_document) { subject.create_document(typed_shape) } - let(:untyped_document) { subject.create_document(foo: 'bar') } - - describe '#create_document' do - context 'untyped input' do - it 'returns a document data' do - expect(untyped_document).to be_a_kind_of(Data) - end - - it 'sets data' do - expect(untyped_document.data).to eq('foo' => 'bar') - end - - it 'sets discriminator to nil' do - expect(untyped_document.discriminator).to be_nil - end - - it 'sets time data using default timestamp format' do - doc = subject.create_document(Time.utc(2024, 12, 25)) - expect(doc.data).to eq(1_735_084_800) - end - end - - context 'runtime shape input' do - let(:invalid_runtime) do - Struct.new(:string, keyword_init: true) do - include Smithy::Schema::Structure - end - end - - it 'sets data' do - expect(typed_document.data).to include(expected_typed_data) - end - - it 'sets discriminator' do - expect(typed_document.discriminator).to eql(structure_shape.id) - end - - it 'raises when runtime shape is not found in type registry' do - expect do - subject.create_document(invalid_runtime.new(string: 'foo')) - end.to raise_error(ArgumentError) - end - end - - context 'parsed json input' do - let(:json) do - { - '__type' => 'smithy.ruby.tests#Structure', - 'string' => 'hello' - } - end - - let(:document) { subject.create_document(json) } - - it 'sets data' do - expect(document.data).to include( - { - '__type' => 'smithy.ruby.tests#Structure', - 'string' => 'hello' - } - ) - end - - it 'sets discriminator' do - expect(document.discriminator).to eql(structure_shape.id) - end - - it 'raises when discriminator is not found in type registry' do - json['__type'] = 'smithy.ruby.tests#Invalid' - expect do - subject.create_document(json) - end.to raise_error(ArgumentError) - end - end - end - - describe '#serialize_document' do - it 'returns serialized data' do - expect(subject.serialize_document(typed_document)) - .to include(expected_typed_data) - end - - it 'returns serialized data with jsonName when applicable' do - shapes['smithy.ruby.tests#Structure']['members']['string']['traits'] = { 'smithy.api#jsonName' => 'A' } - shapes['smithy.ruby.tests#Union']['members']['string']['traits'] = { 'smithy.api#jsonName' => 'B' } - - document = subject.create_document(structure_shape.type.new(string: 'hello', union: { string: 'world' })) - expect(subject.serialize_document(document, use_json_name: true)).to include( - '__type' => 'smithy.ruby.tests#Structure', - 'A' => 'hello', - 'union' => { 'B' => 'world' } - ) - end - - it 'returns serialized data with timestampTrait when applicable' do - shapes['smithy.ruby.tests#Structure']['members']['timestampDateTime'] = { - 'target' => 'smithy.api#Timestamp', - 'traits' => { 'smithy.api#timestampFormat' => 'date-time' } - } - shapes['smithy.ruby.tests#Structure']['members']['timestampHttpDate'] = { - 'target' => 'smithy.api#Timestamp', - 'traits' => { 'smithy.api#timestampFormat' => 'http-date' } - } - shapes['smithy.ruby.tests#Structure']['members']['timestampEpochSeconds'] = { - 'target' => 'smithy.api#Timestamp', - 'traits' => { 'smithy.api#timestampFormat' => 'epoch-seconds' } - } - shapes['smithy.ruby.tests#Structure']['members']['timestampUseShape'] = { - 'target' => 'smithy.ruby.tests#TimestampUseShape' - } - shapes['smithy.ruby.tests#TimestampUseShape'] = { - 'type' => 'timestamp', - 'traits' => { 'smithy.api#timestampFormat' => 'http-date' } - } - - struct = structure_shape.type.new( - timestamp_date_time: Time.utc(2024, 12, 25), - timestamp_http_date: Time.utc(2024, 12, 25), - timestamp_epoch_seconds: Time.utc(2024, 12, 25), - timestamp_use_shape: Time.utc(2024, 12, 25) - ) - - document = subject.create_document(struct) - expect(subject.serialize_document(document, use_timestamp_format: true)).to include( - '__type' => 'smithy.ruby.tests#Structure', - 'timestampDateTime' => '2024-12-25T00:00:00Z', - 'timestampHttpDate' => 'Wed, 25 Dec 2024 00:00:00 GMT', - 'timestampEpochSeconds' => 1_735_084_800, - 'timestampUseShape' => 'Wed, 25 Dec 2024 00:00:00 GMT' - ) - end - - it 'raises when an invalid document is given' do - expect do - subject.serialize_document('foo') - end.to raise_error(ArgumentError) - expect do - subject.serialize_document(untyped_document) - end.to raise_error(ArgumentError) - end - - it 'raises when discriminator cannot be found in type registry' do - # TODO - end - end - end - - describe Deserializer do - it 'TBD' do - # TODO - end - end - context 'SERDE test cases' do # don't look ... its not ready >_< - tests = JSON.load_file(File.expand_path('../fixtures/typed-documents/test-cases.json', __dir__.to_s)) - let(:test_model) { JSON.load_file(File.expand_path('../fixtures/typed-documents/model.json', __dir__.to_s)) } + tests = JSON.load_file(File.expand_path('../fixtures/documents/test-cases.json', __dir__.to_s)) + let(:test_model) { JSON.load_file(File.expand_path('../fixtures/documents/model.json', __dir__.to_s)) } let(:schema) { SchemaHelper.sample_schema(model: test_model) } let(:structure_shape) { schema.const_get(:OmniWidget) } let(:type_registry) { schema.const_get(:TYPE_REGISTRY) } From 8cc697c0a07a211752d7b7d821e675a3ce5a79c6 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Wed, 7 May 2025 13:15:20 -0700 Subject: [PATCH 47/54] Remove TimeHelper --- gems/smithy-schema/lib/smithy-schema.rb | 3 +- .../lib/smithy-schema/time_helper.rb | 31 ----------------- .../spec/smithy-schema/time_helper_spec.rb | 34 ------------------- 3 files changed, 1 insertion(+), 67 deletions(-) delete mode 100644 gems/smithy-schema/lib/smithy-schema/time_helper.rb delete mode 100644 gems/smithy-schema/spec/smithy-schema/time_helper_spec.rb diff --git a/gems/smithy-schema/lib/smithy-schema.rb b/gems/smithy-schema/lib/smithy-schema.rb index e07cffe29..976dae384 100644 --- a/gems/smithy-schema/lib/smithy-schema.rb +++ b/gems/smithy-schema/lib/smithy-schema.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true +require_relative 'smithy-schema/document' require_relative 'smithy-schema/shapes' require_relative 'smithy-schema/structure' -require_relative 'smithy-schema/document' -require_relative 'smithy-schema/time_helper' require_relative 'smithy-schema/type_registry' require_relative 'smithy-schema/union' diff --git a/gems/smithy-schema/lib/smithy-schema/time_helper.rb b/gems/smithy-schema/lib/smithy-schema/time_helper.rb deleted file mode 100644 index 43ae61201..000000000 --- a/gems/smithy-schema/lib/smithy-schema/time_helper.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -module Smithy - module Schema - # A module that provides helper methods to convert Time objects - # based on the given TimestampFormat trait. - # @api private - # TODO: need to handle fractional secs - module TimeHelper - class << self - # @param [Time] time - # @param [String] trait TimestampFormat trait value - # @return [Object] The time as TimestampFormat trait format - def time(time, trait) - raise ArgumentError, 'expected Time as input' unless time.is_a?(Time) - - case trait - when 'http-date' - time.utc.httpdate - when 'date-time' - time.utc.iso8601 - when 'epoch-seconds' - time.utc.to_i - else - raise ArgumentError, "unhandled timestamp format `#{trait}`" - end - end - end - end - end -end diff --git a/gems/smithy-schema/spec/smithy-schema/time_helper_spec.rb b/gems/smithy-schema/spec/smithy-schema/time_helper_spec.rb deleted file mode 100644 index 8ddad8930..000000000 --- a/gems/smithy-schema/spec/smithy-schema/time_helper_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -require_relative '../spec_helper' -require 'time' - -module Smithy - module Schema - describe TimeHelper do - describe '#time' do - let(:time) { Time.utc(2002, 10, 31) } - - it 'returns as http-date format' do - expect(subject.time(time, 'http-date')).to eq('Thu, 31 Oct 2002 00:00:00 GMT') - end - - it 'returns as date-time format' do - expect(subject.time(time, 'date-time')).to eq('2002-10-31T00:00:00Z') - end - - it 'returns as epoch-seconds format' do - expect(subject.time(time, 'epoch-seconds')).to eq(1_036_022_400) - end - - it 'raises when given time is invalid ' do - expect { subject.time('time', 'http-date') }.to raise_error(ArgumentError) - end - - it 'raises when given timestamp trait is unhandled' do - expect { subject.time(time, 'foo') }.to raise_error(ArgumentError) - end - end - end - end -end From 75141d191e49751fe56f9aed1c7cf9b8ad302684 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Wed, 7 May 2025 13:17:35 -0700 Subject: [PATCH 48/54] Require delegate --- gems/smithy-schema/lib/smithy-schema/document.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/gems/smithy-schema/lib/smithy-schema/document.rb b/gems/smithy-schema/lib/smithy-schema/document.rb index 5a7a551dc..20f644f5b 100644 --- a/gems/smithy-schema/lib/smithy-schema/document.rb +++ b/gems/smithy-schema/lib/smithy-schema/document.rb @@ -2,6 +2,7 @@ require_relative 'document/deserializer' require_relative 'document/serializer' +require 'delegate' module Smithy module Schema From d36a70ba5f5f404f6a9991df40221c4612ea6823 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Wed, 7 May 2025 13:20:38 -0700 Subject: [PATCH 49/54] Fix relative ordering --- gems/smithy-schema/lib/smithy-schema.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gems/smithy-schema/lib/smithy-schema.rb b/gems/smithy-schema/lib/smithy-schema.rb index 976dae384..af2a4f204 100644 --- a/gems/smithy-schema/lib/smithy-schema.rb +++ b/gems/smithy-schema/lib/smithy-schema.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require_relative 'smithy-schema/document' require_relative 'smithy-schema/shapes' require_relative 'smithy-schema/structure' require_relative 'smithy-schema/type_registry' require_relative 'smithy-schema/union' +require_relative 'smithy-schema/document' module Smithy # Base module for Smithy schema classes. From b2a699863cae8f31e59f7d353d37ad076271eebf Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Wed, 7 May 2025 14:55:45 -0700 Subject: [PATCH 50/54] Fix type registry test --- gems/smithy/spec/support/examples/schema_examples.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gems/smithy/spec/support/examples/schema_examples.rb b/gems/smithy/spec/support/examples/schema_examples.rb index 39f9c689c..685527fd1 100644 --- a/gems/smithy/spec/support/examples/schema_examples.rb +++ b/gems/smithy/spec/support/examples/schema_examples.rb @@ -391,8 +391,7 @@ def expect_generated_shape(subject, shape_class, shape_hash) let(:typed_shapes) do fixture['shapes'].select do |_k, v| - %w[union structure].include?(v['type']) && - !v['traits']&.include?('smithy.api#trait') + (v['type'] == 'structure') && !v['traits']&.include?('smithy.api#trait') end end From 9219b13cc510b19504344efa3dfa11e53bea4738 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Wed, 7 May 2025 15:02:12 -0700 Subject: [PATCH 51/54] Remove unnecessary shape --- gems/smithy-schema/spec/support/schema_helper.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/gems/smithy-schema/spec/support/schema_helper.rb b/gems/smithy-schema/spec/support/schema_helper.rb index 6730445ee..156dab235 100644 --- a/gems/smithy-schema/spec/support/schema_helper.rb +++ b/gems/smithy-schema/spec/support/schema_helper.rb @@ -100,10 +100,7 @@ def sample_shapes 'smithy.ruby.tests#Union' => { 'type' => 'union', 'members' => { - 'string' => { - 'target' => 'smithy.api#String', - 'traits' => { 'smithy.api#jsonName' => 'jsonName' } - }, + 'string' => { 'target' => 'smithy.api#String'}, 'structure' => { 'target' => 'smithy.ruby.tests#Structure' }, 'unit' => { 'target' => 'smithy.api#Unit' } } From 1f0b771567c74b579634efbe150ebd02bb5481b7 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Wed, 7 May 2025 15:02:48 -0700 Subject: [PATCH 52/54] Rubocop fix --- gems/smithy-schema/spec/support/schema_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gems/smithy-schema/spec/support/schema_helper.rb b/gems/smithy-schema/spec/support/schema_helper.rb index 156dab235..1fc6d4848 100644 --- a/gems/smithy-schema/spec/support/schema_helper.rb +++ b/gems/smithy-schema/spec/support/schema_helper.rb @@ -100,7 +100,7 @@ def sample_shapes 'smithy.ruby.tests#Union' => { 'type' => 'union', 'members' => { - 'string' => { 'target' => 'smithy.api#String'}, + 'string' => { 'target' => 'smithy.api#String' }, 'structure' => { 'target' => 'smithy.ruby.tests#Structure' }, 'unit' => { 'target' => 'smithy.api#Unit' } } From 74450a16a982954da1b23a2df0b952d97a847605 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Thu, 8 May 2025 14:30:18 -0700 Subject: [PATCH 53/54] More changes --- .../smithy-schema/document/deserializer.rb | 6 +- .../lib/smithy-schema/document/serializer.rb | 39 ++- .../lib/smithy-schema/type_registry.rb | 2 +- .../spec/fixtures/documents/model.json | 150 ---------- .../spec/fixtures/documents/test-cases.json | 278 ------------------ .../document/deserializer_spec.rb | 2 +- .../spec/smithy-schema/document_spec.rb | 62 ---- 7 files changed, 30 insertions(+), 509 deletions(-) delete mode 100644 gems/smithy-schema/spec/fixtures/documents/model.json delete mode 100644 gems/smithy-schema/spec/fixtures/documents/test-cases.json diff --git a/gems/smithy-schema/lib/smithy-schema/document/deserializer.rb b/gems/smithy-schema/lib/smithy-schema/document/deserializer.rb index ff4a99e65..3d48d1f49 100644 --- a/gems/smithy-schema/lib/smithy-schema/document/deserializer.rb +++ b/gems/smithy-schema/lib/smithy-schema/document/deserializer.rb @@ -84,7 +84,7 @@ def document(values) return values unless values.is_a?(Hash) && values.key?('__type') msg = 'invalid document - document discriminator not found in type registry' - raise ArgumentError, msg unless @type_registry.key? + raise ArgumentError, msg unless @type_registry.key?(values['__type']) shape_ref = ShapeRef.new(shape: @type_registry[values['__type']]) shape(shape_ref, values) @@ -151,7 +151,7 @@ def timestamp(value) end def union(ref, values, target = nil) # rubocop:disable Metrics/AbcSize - validate_union!(values) + validate_union(values) key, value = values.first return if key.nil? @@ -166,7 +166,7 @@ def union(ref, values, target = nil) # rubocop:disable Metrics/AbcSize ref.shape.member_type(:unknown).new(key, value) end - def validate_union!(values) + def validate_union(values) return unless values.size > 1 msg = "union value includes more than one key, received: #{values.keys}" diff --git a/gems/smithy-schema/lib/smithy-schema/document/serializer.rb b/gems/smithy-schema/lib/smithy-schema/document/serializer.rb index 0e9e7680e..dbeb6b555 100644 --- a/gems/smithy-schema/lib/smithy-schema/document/serializer.rb +++ b/gems/smithy-schema/lib/smithy-schema/document/serializer.rb @@ -62,7 +62,7 @@ def create_document(data) else if discriminator?(data) shape = @type_registry[data['__type']] - Data.new(format_document_data(shape, data), discriminator: shape.id) + Data.new(format_document_data(shape, data, discriminator: true), discriminator: shape.id) else Data.new(serialize_untyped(data)) end @@ -127,14 +127,20 @@ def blob(value, opts) end def document(values, opts) - if values.is_a?(Shapes::Structure) - shape = @type_registry.shape_by_type(values.class) - data = shape(ShapeRef.new(shape: shape), values, opts) - data['__type'] = shape.id - data - else - values - end + return values unless typed_document?(values) + + shape = + if values.is_a?(Smithy::Schema::Structure) + @type_registry.shape_by_type(values.class) + else + @type_registry[values['__type']] + end + format_document_data(shape, values, opts) + end + + def typed_document?(values) + (values.is_a?(Smithy::Schema::Structure) && @type_registry.shape_by_type(values.class)) || + (values.is_a?(Hash) && values.key?('__type')) end def float(value, _opts) @@ -142,10 +148,10 @@ def float(value, _opts) 'Infinity' elsif value == -::Float::INFINITY '-Infinity' - elsif value.nan? + elsif value.to_f.nan? 'NaN' else - value + value.to_f end end @@ -177,10 +183,10 @@ def structure(ref, values, opts) end def timestamp(ref, value, opts) + value = normalize_timestamp_value(value) return value.to_i unless opts[:use_timestamp_format] trait = 'smithy.api#timestampFormat' - value = normalize_timestamp_value(value) case ref.traits[trait] || ref.shape.traits[trait] when 'date-time' then value.utc.iso8601 when 'http-date' then value.utc.httpdate @@ -226,8 +232,13 @@ def normalize_timestamp_value(value) end def resolve_member_ref(ref, name) - ref.shape.member(name) || - (ref.shape.members.values.find { |r| r.member_name == name }) + ref.shape.member(name) || find_member_ref_by_names(ref, name) + end + + def find_member_ref_by_names(ref, name) + ref.shape.members.values.find do |r| + r.traits['smithy.api#jsonName'] == name || r.member_name == name + end end def resolve_member_name(member_ref, opts) diff --git a/gems/smithy-schema/lib/smithy-schema/type_registry.rb b/gems/smithy-schema/lib/smithy-schema/type_registry.rb index a76e9b272..a9bfd5c15 100644 --- a/gems/smithy-schema/lib/smithy-schema/type_registry.rb +++ b/gems/smithy-schema/lib/smithy-schema/type_registry.rb @@ -3,7 +3,7 @@ module Smithy module Schema # A registry that contains a map of Smithy shape ID to its shape defined in a schema. - # The registered shapes are limited to {Shapes::StructureShape} with a type representation. + # The registered shapes are limited to {Shapes::StructureShape} with a type. # # This registry has the following functionalities: # diff --git a/gems/smithy-schema/spec/fixtures/documents/model.json b/gems/smithy-schema/spec/fixtures/documents/model.json deleted file mode 100644 index f5cd7df14..000000000 --- a/gems/smithy-schema/spec/fixtures/documents/model.json +++ /dev/null @@ -1,150 +0,0 @@ -{ - "smithy": "2.0", - "shapes": { - "smithy.ruby.tests#SampleSchema": { - "type" : "service", - "operations" : [ - { "target" : "smithy.example#Operation" } - ] - }, - "smithy.example#Operation": { - "type": "operation", - "input": { - "target": "smithy.example#OmniWidget" - }, - "output": { - "target": "smithy.example#OmniWidget" - } - }, - "smithy.example#OmniWidget": { - "type": "structure", - "members": { - "blob": { - "target": "smithy.api#Blob" - }, - "boolean": { - "target": "smithy.api#Boolean" - }, - "string": { - "target": "smithy.api#String", - "traits": { - "smithy.api#jsonName": "String", - "smithy.api#xmlName": "String" - } - }, - "byte": { - "target": "smithy.api#Byte" - }, - "short": { - "target": "smithy.api#Short" - }, - "integer": { - "target": "smithy.api#Integer" - }, - "long": { - "target": "smithy.api#Long" - }, - "float": { - "target": "smithy.api#Float" - }, - "double": { - "target": "smithy.api#Double" - }, - "bigInteger": { - "target": "smithy.api#BigInteger" - }, - "bigDecimal": { - "target": "smithy.api#BigDecimal" - }, - "timestamp": { - "target": "smithy.api#Timestamp" - }, - "timestampDateTime": { - "target": "smithy.api#Timestamp", - "traits": { - "smithy.api#timestampFormat": "date-time" - } - }, - "timestampHttpDate": { - "target": "smithy.api#Timestamp", - "traits": { - "smithy.api#timestampFormat": "http-date" - } - }, - "timestampEpochSeconds": { - "target": "smithy.api#Timestamp", - "traits": { - "smithy.api#timestampFormat": "epoch-seconds" - } - }, - "document": { - "target": "smithy.api#Document" - }, - "enum": { - "target": "smithy.example#ABEnum" - }, - "intEnum": { - "target": "smithy.example#ABIntEnum" - }, - "list": { - "target": "smithy.example#OmniWidgetList" - }, - "map": { - "target": "smithy.example#OmniWidgetMap" - }, - "structure": { - "target": "smithy.example#OmniWidget" - } - } - }, - "smithy.example#OmniWidgetList": { - "type": "list", - "member": { - "target": "smithy.example#OmniWidget" - } - }, - "smithy.example#OmniWidgetMap": { - "type": "map", - "key": { - "target": "smithy.api#String" - }, - "value": { - "target": "smithy.example#OmniWidget" - } - }, - "smithy.example#ABEnum": { - "type": "enum", - "members": { - "A": { - "target": "smithy.api#Unit", - "traits": { - "smithy.api#enumValue": "A" - } - }, - "B": { - "target": "smithy.api#Unit", - "traits": { - "smithy.api#enumValue": "B" - } - } - } - }, - "smithy.example#ABIntEnum": { - "type": "intEnum", - "members": { - "A": { - "target": "smithy.api#Unit", - "traits": { - "smithy.api#enumValue": 0 - } - }, - "B": { - "target": "smithy.api#Unit", - "traits": { - "smithy.api#enumValue": 1 - } - } - } - } - } -} \ No newline at end of file diff --git a/gems/smithy-schema/spec/fixtures/documents/test-cases.json b/gems/smithy-schema/spec/fixtures/documents/test-cases.json deleted file mode 100644 index 6fabdc750..000000000 --- a/gems/smithy-schema/spec/fixtures/documents/test-cases.json +++ /dev/null @@ -1,278 +0,0 @@ -{ - "serdeTests": [ - { - "name": "default test case", - "subject": "smithy.example#OmniWidget", - "serialized": { - "__type": "smithy.example#OmniWidget", - "string": "hello" - }, - "deserialized": { - "string": "hello" - }, - "codec": "json", - "settings": { - "jsonName": false - } - }, - { - "name": "jsonName", - "subject": "smithy.example#OmniWidget", - "serialized": { - "__type": "smithy.example#OmniWidget", - "String": "hello" - }, - "deserialized": { - "string": "hello" - }, - "codec": "json", - "settings": { - "jsonName": true - } - }, - { - "name": "timestampFormat, with default epoch-seconds", - "subject": "smithy.example#OmniWidget", - "serialized": { - "__type": "smithy.example#OmniWidget", - "timestamp": 0, - "timestampDateTime": "1970-01-01T00:00:00Z", - "timestampHttpDate": "Thu, 01 Jan 1970 00:00:00 GMT", - "timestampEpochSeconds": 0 - }, - "deserialized": { - "timestamp": "1970-01-01T00:00:00Z", - "timestampDateTime": "1970-01-01T00:00:00Z", - "timestampHttpDate": "1970-01-01T00:00:00Z", - "timestampEpochSeconds": "1970-01-01T00:00:00Z" - }, - "codec": "json", - "settings": { - "timestampFormat": { - "useTrait": true, - "default": "epoch-seconds" - } - } - }, - { - "name": "ignoring timestampFormat, with default epoch-seconds", - "subject": "smithy.example#OmniWidget", - "serialized": { - "__type": "smithy.example#OmniWidget", - "timestamp": 0, - "timestampDateTime": 0, - "timestampHttpDate": 0, - "timestampEpochSeconds": 0 - }, - "deserialized": { - "timestamp": "1970-01-01T00:00:00Z", - "timestampDateTime": "1970-01-01T00:00:00Z", - "timestampHttpDate": "1970-01-01T00:00:00Z", - "timestampEpochSeconds": "1970-01-01T00:00:00Z" - }, - "codec": "json", - "settings": { - "timestampFormat": { - "useTrait": false, - "default": "epoch-seconds" - } - } - }, - { - "name": "default settings with populated members with initial values", - "subject": "smithy.example#OmniWidget", - "serialized": { - "__type": "smithy.example#OmniWidget", - "blob": "YWJjZA==", - "boolean": false, - "String": "", - "byte": 0, - "short": 0, - "integer": 0, - "long": 0, - "float": 0, - "double": 0, - "bigInteger": 0, - "bigDecimal": 0, - "timestamp": 0, - "timestampDateTime": "1970-01-01T00:00:00Z", - "timestampHttpDate": "Thu, 01 Jan 1970 00:00:00 GMT", - "timestampEpochSeconds": 0, - "document": {}, - "enum": "A", - "intEnum": 0, - "list": [], - "map": {}, - "structure": {} - }, - "deserialized": { - "blob": "YWJjZA==", - "boolean": false, - "string": "", - "byte": 0, - "short": 0, - "integer": 0, - "long": 0, - "float": 0.0, - "double": 0.0, - "bigInteger": 0, - "bigDecimal": 0, - "timestamp": 0, - "timestampDateTime": "1970-01-01T00:00:00Z", - "timestampHttpDate": "Thu, 01 Jan 1970 00:00:00 GMT", - "timestampEpochSeconds": 0, - "document": {}, - "enum": "A", - "intEnum": 0, - "list": [], - "map": { - }, - "structure": { - } - }, - "codec": "json", - "settings": { - "defaultNamespace": "smithy.example", - "jsonName": true, - "timestampFormat": { - "useTrait": true, - "default": "epoch-seconds" - } - } - }, - { - "name": "default settings with populated members with filled values", - "subject": "smithy.example#OmniWidget", - "serialized": { - "__type": "smithy.example#OmniWidget", - "blob": "YWJjZA==", - "boolean": true, - "string": "abcd", - "byte": -128, - "short": -32768, - "integer": -2147483648, - "long": -9223372036854775808, - "float": -0.1, - "double": -0.01, - "bigInteger": -9223372036854775809, - "bigDecimal": -9223372036854775809.1, - "timestamp": 17514144000, - "timestampDateTime": "2525-01-01T00:00:00Z", - "timestampHttpDate": "Mon, 01 Jan 2525 00:00:00 GMT", - "timestampEpochSeconds": 17514144000, - "document": { - "__type": "smithy.example#OmniWidget", - "timestamp": 17514144000, - "timestampDateTime": "2525-01-01T00:00:00Z", - "timestampHttpDate": "Mon, 01 Jan 2525 00:00:00 GMT", - "timestampEpochSeconds": 17514144000, - "document": true - }, - "enum": "B", - "intEnum": 1, - "list": [ - { - "byte": -128 - }, - { - "short": -32768 - } - ], - "map": { - "a": { - "integer": -2147483648 - }, - "b": { - "long": -9223372036854775808 - } - }, - "structure": { - "list": [ - { - "double": -0.01 - }, - { - "float": -0.1 - } - ], - "map": { - "a": { - "bigInteger": -9223372036854775809 - }, - "b": { - "bigDecimal": -9223372036854775809.1 - } - } - } - }, - "deserialized": { - "blob": "YWJjZA==", - "boolean": true, - "string": "abcd", - "byte": -128, - "short": -32768, - "integer": -2147483648, - "long": -9223372036854775808, - "float": -0.1, - "double": -0.01, - "bigInteger": -9223372036854775809, - "bigDecimal": -9223372036854775809.1, - "timestamp": "2525-01-01T00:00:00Z", - "timestampDateTime": "2525-01-01T00:00:00Z", - "timestampHttpDate": "2525-01-01T00:00:00Z", - "timestampEpochSeconds": "2525-01-01T00:00:00Z", - "document": { - "timestamp": "2525-01-01T00:00:00Z", - "timestampDateTime": "2525-01-01T00:00:00Z", - "timestampHttpDate": "2525-01-01T00:00:00Z", - "timestampEpochSeconds": "2525-01-01T00:00:00Z" - }, - "enum": "B", - "intEnum": 1, - "list": [ - { - "byte": -128 - }, - { - "short": -32768 - } - ], - "map": { - "a": { - "integer": -2147483648 - }, - "b": { - "long": -9223372036854775808 - } - }, - "structure": { - "list": [ - { - "double": -0.01 - }, - { - "float": -0.1 - } - ], - "map": { - "a": { - "bigInteger": -9223372036854775809 - }, - "b": { - "bigDecimal": -9223372036854775809.1 - } - } - } - }, - "codec": "json", - "settings": { - "defaultNamespace": "smithy.example", - "jsonName": true, - "timestampFormat": { - "useTrait": true, - "default": "epoch-seconds" - } - } - } - ] -} \ No newline at end of file diff --git a/gems/smithy-schema/spec/smithy-schema/document/deserializer_spec.rb b/gems/smithy-schema/spec/smithy-schema/document/deserializer_spec.rb index 179166a7c..13537135c 100644 --- a/gems/smithy-schema/spec/smithy-schema/document/deserializer_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/document/deserializer_spec.rb @@ -17,7 +17,6 @@ module Document let(:sample_schema) { SchemaHelper.sample_schema(shapes: shapes) } let(:type_registry) { sample_schema.const_get(:TYPE_REGISTRY) } let(:structure_shape) { sample_schema.const_get(:Structure) } - let(:another_shape) { sample_schema.const_get(:Foo) } let(:typed_shape) do structure_shape.type.new( big_decimal: 0, @@ -78,6 +77,7 @@ module Document end it 'prioritizes provided shape over document discriminator when deserializing' do + another_shape = sample_schema.const_get(:Foo) runtime_shape = subject.deserialize(typed_document, shape: another_shape) expect(runtime_shape).to be_a_kind_of(Structure) expect(runtime_shape).to be_an_instance_of(another_shape.type) diff --git a/gems/smithy-schema/spec/smithy-schema/document_spec.rb b/gems/smithy-schema/spec/smithy-schema/document_spec.rb index c9239be8a..94c5bdc70 100644 --- a/gems/smithy-schema/spec/smithy-schema/document_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/document_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require_relative '../spec_helper' -require_relative '../support/schema_helper' module Smithy module Schema @@ -35,67 +34,6 @@ module Document end end end - - context 'SERDE test cases' do # don't look ... its not ready >_< - tests = JSON.load_file(File.expand_path('../fixtures/documents/test-cases.json', __dir__.to_s)) - let(:test_model) { JSON.load_file(File.expand_path('../fixtures/documents/model.json', __dir__.to_s)) } - let(:schema) { SchemaHelper.sample_schema(model: test_model) } - let(:structure_shape) { schema.const_get(:OmniWidget) } - let(:type_registry) { schema.const_get(:TYPE_REGISTRY) } - - # def create_runtime_shape(data, shape) - # Document.new(data, shape: shape).as_typed(shape) - # end - - # def document_options(settings) - # settings.each_with_object({}) do |(k, v), o| - # case k - # when 'jsonName' - # o[:disable_json_name] = true if v == false - # when 'timestampFormat' - # o[:disable_timestamp_format] = true if v['useTrait'] == false - # end - # end - # end - - tests['serdeTests'].each do |test_case| - context "Case: #{test_case['name']}" do - # let(:data_object) { create_runtime_shape(test_case['deserialized'], structure_shape) } - # let(:document_object) { Document.new(test_case['serialized'], shape: structure_shape) } - # let(:serialized_data) { test_case['serialized'] } - # let(:settings) { document_options(test_case['settings']) } - - it 'when data object is converted to a Document, it deeply equals the document object' do - # document = Document.new(data_object, shape: structure_shape) - # expect(document.data).to eq(document_object.data) - end - - it 'when document object is deserialized, it deeply equals data object' do - # expect(document_object.as_typed(structure_shape).to_h).to eq(data_object.to_h) - end - - it 'when data object is serialized, it equals serialized data' do - # document = Document.new(data_object, shape: structure_shape) - # expect(document.as_json(structure_shape, settings)).to eq(serialized_data.except('__type')) - end - - it 'when serialized data is deserialized, it equals data object' do - # document = Document.new(serialized_data, shape: structure_shape) - # expect(document.as_typed(structure_shape).to_h).to eq(data_object.to_h) - end - - it 'when document object is serialized, it equals serialized data' do - # serialized_document = document_object.as_json(structure_shape, settings) - # expect(serialized_document).to eq(serialized_data.except('__type')) - end - - # Assert that the serialized test data (3) can be parsed into a document (2) - it 'serialized data can be parsed into document' do - # expect(document_object).to be_an_instance_of(Document) - end - end - end - end end end end From 68c94c16f6e8f940f5d4cf8cfa8e1334ea74932d Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Thu, 8 May 2025 14:40:21 -0700 Subject: [PATCH 54/54] Add docs --- .../lib/smithy-schema/document.rb | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/gems/smithy-schema/lib/smithy-schema/document.rb b/gems/smithy-schema/lib/smithy-schema/document.rb index 20f644f5b..923c39da7 100644 --- a/gems/smithy-schema/lib/smithy-schema/document.rb +++ b/gems/smithy-schema/lib/smithy-schema/document.rb @@ -6,25 +6,40 @@ module Smithy module Schema + # The module provides functionality for handling Smithy document types, + # which represent protocol-agnostic data structures in the Smithy data model. + # + # This module includes capabilities for: + # * Serialization and deserialization of document data + # * Type-aware data handling + # * Support for JSON document format + # + # @example Basic usage with a document + # data = Document::Data.new({ name: "document" }) + # data.data # => { "name" => "example" } + # + # @example Using with a shape + # shape = Smithy::Schema::StructureShape.new + # data = Document::Data.new({ "name" => "example" }, shape: shape) + # module Document - # A Smithy document type, representing typed or untyped data from Smithy data model. - # ## Document types - # Document types are protocol-agnostic view of untyped data. They could be combined - # with a shape to serialize its contents. - # - # Smithy-Ruby currently only support JSON documents. + # A Smithy document, representing typed or untyped data from the Smithy data model. + # The Data class delegates to the underlying data object while providing additional + # document-specific functionality. class Data < ::SimpleDelegator - # @param [Object] data document data + # @param [Object] data document data that is in JSON-friendly format # @param [Hash] options - # @option options [Smithy::Schema::StructureShape] :shape shape to reference when setting - # document data. + # @option options [String] :discriminator This value is used to identify a specific + # shape. This is equivalent of a Smithy shape ID. def initialize(data, options = {}) @data = data super(@data) @discriminator = options[:discriminator] || nil end - # @return [String] discriminator + # Returns the discriminator value for the document + # + # @return [String. nil] discriminator attr_reader :discriminator def data