Skip to content

Defaults #297

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
May 17, 2025
Merged

Defaults #297

merged 21 commits into from
May 17, 2025

Conversation

mullermp
Copy link
Contributor

@mullermp mullermp commented May 7, 2025

Handle the default trait.

Currently have two approaches to consider:

  1. Handle via types. The biggest issue here is that input types are not used. It will require work to convert hashy input into types prior to serialization.

There are two sub-approaches - one that overrides initialize and the other that sets a defaults instance variable for Smithy::Schema::Structure to use implicitly.

  1. Handle via codecs. This approach would not change anything major, but it requires all codecs to understand defaults. It also means that types do not have defaults built in. The codec would look at the default value and apply it if applicable.

(Note: we could implement both approaches but that seems wasteful)

I chose option 2 here after going down a rabbit hole. It seems cbor protocol should not serialize top level members on the client. Because of this, types would need to know protocol specific information. Protocols have client and server implementations, making it ideal to handle it at runtime as codecs (with a configurable option regarding top level).

I chose option 1 again, by using synthetic input/output. These synthetic shapes will have input/output traits, and if those traits are present, defaults generation does not apply.

@mullermp mullermp marked this pull request as ready for review May 7, 2025 23:40
Copy link
Contributor

@jterapin jterapin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, I'll follow this PR format to update my Document PR to stay aligned. We should chat about what you mean by going with option 1.

Copy link
Contributor

@alextwoods alextwoods left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice - looks good overall - I think the approach makes sense

@@ -1,5 +1,7 @@
# frozen_string_literal: true

require 'base64'
require 'bigdecimal'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be adding bigdecimal as a required, general dependency? If I remember it can cause some issues for users without compilers/jruby and SDKs dont generally support it. Maybe add it as a dependency in client gems only when the model requires it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think @hsbt has work to make it compatible everywhere. We can check.

it 'deserializes unknown union members' do
unknown_union_type = shape.member(:union).shape.member_type(:unknown)
data = { union: { 'someThing' => 'someValue' } }
deserialized = subject.deserialize(shape, CBOR.encode(data))
expect(deserialized.union).to be_a(unknown_union_type)
expect(deserialized.union.to_h).to eq(unknown: { name: 'someThing', value: 'someValue' })
end

it 'raises when deserializing unions with more than one member' do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we're removing these tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was wrong behavior when I had chatted with Kevin. I'm not really sure if we should be raising. Let me think on this.

@@ -33,7 +33,7 @@ def initialize(options = {})
# @return [Base]
attr_accessor :client

# @return [Hash] The hash of request parameters.
# @return [Hash, Struct] The request parameters as a Hash or a Struct.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume Struct here would be the runtime input shape?

This is a little confusing - I assume the reason for the union of types here is that at the start of the request we have hash params and after the parameter conversion handler runs, we have runtime shapes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, at the start of the request, we would have a hash OR struct, but eventually it will always be a struct. I'm not sure if we want to do this though - but it allows us to get values from types (like defaults) if we use the input type.


next unless ref.shape.member?(k)
type = @convert_structures ? ref.shape.type.new : values
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When convert_structures is false, this should copy values right? Otherwise the input is being modified

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dup is called by the converter prior to this function call if it's a hash.


def default?
traits = @member.fetch('traits', {})
traits.key?('smithy.api#default') && !traits.key?('smithy.api#clientOptional')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gets a little confusing for shape only gems / server generation - the smithy docs say

The clientOptional trait applied to a member marked with the default trait causes non-authoritative generators to ignore the default trait.

So, in a client context, we should use it, but in an authoritative (server) type we shouldn't. I'm not sure if theres a good way to handle that in a server/client agnostic generation though and since we're targeting client generation, maybe this is good for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. Good call out.. I'm not sure how else to handle this? I'll chat with Michael.


def document(default)
case default
when nil then 'nil'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should a check for nil default be done above and default just be skipped completely if its nil or is this a special document case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a document case.

input_shape = {
'type' => 'structure',
'traits' => target.fetch('traits', {}).merge({ 'smithy.api#input' => {} }),
'members' => target.fetch('members', {}).merge({})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to do a deep copy of members and traits so that another weld that modifies the synthetic shape don't modify the original. I know the merge will do a shallow copy and I can't remember the details of how these are structured so this may not apply.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. This was on my radar. We need a deep copy.

module Smithy
module Welds
# Creates synthetic input and output shapes for operations that do not have them.
class SyntheticInputOutput < Weld
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember all of the history/details on this - but I remember with our java generator we had similar logic which we ended up removing because the logic was pushed up stream.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it needs to be handled by us - the java ModelTransformer does this effectively for us, but I don't have that. The issue is that defaults only apply to shapes not input/output, but if you have synthetics, it not only helps with model evolution, but it allows you to apply defaults at the non-top-level scope which is required by protocol tests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In thinking of speculative "typed document" cases - us manipulating the shape ids on our end will have a trickle effect when we receive a typed document from a service and the underlying discriminator does not match our structures within type registry (since we replaced the shape id with a new one) - unless we intend to keep both synthetic and modeled shapes.

Disregard this comment if this was handled already.

@mullermp mullermp merged commit 715fa2a into decaf May 17, 2025
17 checks passed
@mullermp mullermp deleted the defaults branch May 17, 2025 14:36
Comment on lines +88 to +91
'traits' => {
'smithy.api#required' => {},
'smithy.api#clientOptional' => {}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we move this closer to the tests itself? Feedback: #282 (comment)

Probably could look over the sample_shapes to see if there's any opportunity to move test-specific traits closer.

module Smithy
module Welds
# Creates synthetic input and output shapes for operations that do not have them.
class SyntheticInputOutput < Weld
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In thinking of speculative "typed document" cases - us manipulating the shape ids on our end will have a trickle effect when we receive a typed document from a service and the underlying discriminator does not match our structures within type registry (since we replaced the shape id with a new one) - unless we intend to keep both synthetic and modeled shapes.

Disregard this comment if this was handled already.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants