Skip to content

Implement Typed Documents and TypeRegistry #282

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
wants to merge 63 commits into
base: decaf
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
d33fcdf
Add type registry prototype class
jterapin Mar 6, 2025
5ff40cc
Add type registry to codegenerated schema
jterapin Mar 6, 2025
66a6285
Update projections
jterapin Mar 6, 2025
c5e45ed
Merge branch 'decaf' into typed_documents
jterapin Mar 7, 2025
2bd15a2
Merge branch 'decaf' into typed_documents
jterapin Mar 10, 2025
03bfa82
Merge branch 'decaf' into typed_documents
jterapin Mar 12, 2025
e6435d5
Update requires
jterapin Apr 3, 2025
0830827
Add initial document implementation
jterapin Apr 3, 2025
877654f
Merge decaf into branch
jterapin Apr 3, 2025
a61318f
Update to include cbor
jterapin Apr 7, 2025
4edfae3
Expand on typed docs
jterapin Apr 7, 2025
ff959f1
Update file names
jterapin Apr 7, 2025
8b9b560
Merge branch 'decaf' into typed_documents
jterapin Apr 9, 2025
598db66
More refactoring
jterapin Apr 11, 2025
3a4c0d1
Merge branch 'decaf' into typed_documents
jterapin Apr 11, 2025
269b2b5
Remove scratches
jterapin Apr 11, 2025
90c58ce
Fix rubocop
jterapin Apr 11, 2025
a1e46cc
Clean up document
jterapin Apr 14, 2025
8b666cd
Clean document specs
jterapin Apr 14, 2025
2ddf4bd
Update TypeRegistry
jterapin Apr 14, 2025
112ddf4
Add documentation
jterapin Apr 14, 2025
6283813
Add TypeRegistry specs
jterapin Apr 14, 2025
efbfa5e
Merge branch 'decaf' into typed_documents
jterapin Apr 14, 2025
88ff845
Add TypeRegistry tests
jterapin Apr 15, 2025
66b2cde
Update projections
jterapin Apr 15, 2025
22998a0
Update syntax
jterapin Apr 15, 2025
9afeacd
Update projections
jterapin Apr 15, 2025
232844c
Merge branch 'decaf' into typed_documents
jterapin Apr 17, 2025
e8920fd
Change schema name to shapes to stay aligned
jterapin Apr 17, 2025
ef8c027
Remove as_typed method from TypeRegistry
jterapin Apr 17, 2025
3854661
Create TimeHelper module
jterapin Apr 18, 2025
48e1b0f
Fix timestamp failures
jterapin Apr 18, 2025
04d0d5b
Refactor type registry per feedbacks
jterapin Apr 21, 2025
b35e09b
Update 1 projection
jterapin Apr 21, 2025
66825be
Update weather projection
jterapin Apr 21, 2025
4efe669
Use SchemaHelper for testing
jterapin Apr 21, 2025
73e0e83
Update TypeRegistry to use SchemaHelper for testing
jterapin Apr 21, 2025
60ef29c
Add TypeRegistry documentation
jterapin Apr 21, 2025
b67cf54
Update example
jterapin Apr 28, 2025
ea9b380
Merge decaf into branch
jterapin Apr 28, 2025
381602b
Remove reference to type registry from client
jterapin Apr 28, 2025
6d41f64
Update projections
jterapin Apr 28, 2025
83fa2bb
Update projections
jterapin Apr 28, 2025
b23a2e9
Document now inherits SimpleDelegator
jterapin Apr 28, 2025
306a97e
Expand on type registry docs
jterapin Apr 28, 2025
8ef3055
Fix bug in timehelper
jterapin Apr 29, 2025
a297963
Slim down the sample shapes
jterapin Apr 29, 2025
8e2d322
Update docs
jterapin Apr 29, 2025
532ba8a
Merge decaf into branch
jterapin May 5, 2025
cb66cec
Only add structures to type registry
jterapin May 5, 2025
9fa984c
Update TypeRegistry to limit to StructureShape
jterapin May 5, 2025
166c8b1
Document revamp
jterapin May 6, 2025
9cef7ee
Rename document test cases
jterapin May 7, 2025
d01a780
Merge branch 'decaf' into typed_documents
jterapin May 7, 2025
e283281
Improve Document Deserializer
jterapin May 7, 2025
a48f7a1
Update Document Serializer and its specs
jterapin May 7, 2025
1913938
Update Document Data class
jterapin May 7, 2025
8cc697c
Remove TimeHelper
jterapin May 7, 2025
75141d1
Require delegate
jterapin May 7, 2025
d36a70b
Fix relative ordering
jterapin May 7, 2025
b2a6998
Fix type registry test
jterapin May 7, 2025
9219b13
Remove unnecessary shape
jterapin May 7, 2025
1f0b771
Rubocop fix
jterapin May 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ 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:
```
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
```
Expand Down
2 changes: 2 additions & 0 deletions gems/smithy-schema/lib/smithy-schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

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.
Expand Down
36 changes: 36 additions & 0 deletions gems/smithy-schema/lib/smithy-schema/document.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

require_relative 'document/deserializer'
require_relative 'document/serializer'
require 'delegate'

module Smithy
module Schema
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
super(@data)
@discriminator = options[:discriminator] || nil
end

# @return [String] discriminator
attr_reader :discriminator

def data
__getobj__
end
end
end
end
end
182 changes: 182 additions & 0 deletions gems/smithy-schema/lib/smithy-schema/document/deserializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# frozen_string_literal: true

module Smithy
module Schema
module Document
# 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

# 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
# # => #<struct SampleService::Types::SampleShape....>
# @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)
# # => #<struct SampleService::Types::SomeStructure....>
def deserialize(document, shape: nil)
validate_input(document, shape)

shape ||= resolve_shape(document)
shape(ShapeRef.new(shape: shape), document.data, shape.type.new)
end

private

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 = 'document discriminator not found in type registry'
raise ArgumentError, msg unless @type_registry.key?(document.discriminator)

@type_registry[document.discriminator]
end

def shape(ref, value, target = nil) # rubocop:disable Metrics/CyclomaticComplexity
case ref.shape
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(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 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 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 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 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 sparse?(shape)
shape.traits.include?('smithy.api#sparse')
end
end
end
end
end
Loading