Skip to content

Defaults#297

Merged
mullermp merged 21 commits into
decaffrom
defaults
May 17, 2025
Merged

Defaults#297
mullermp merged 21 commits into
decaffrom
defaults

Conversation

@mullermp
Copy link
Copy Markdown
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.

Comment thread gems/smithy-schema/lib/smithy-schema/structure.rb Outdated
@mullermp mullermp marked this pull request as ready for review May 7, 2025 23:40
Copy link
Copy Markdown
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.

Comment thread gems/smithy-cbor/lib/smithy-cbor/deserializer.rb
Comment thread gems/smithy-cbor/lib/smithy-cbor/deserializer.rb
Comment thread gems/smithy-client/lib/smithy-client/signers/http_basic.rb
Comment thread gems/smithy-client/lib/smithy-client/stubbing/data_applicator.rb Outdated
Comment thread gems/smithy-json/spec/smithy-json/codec_spec.rb
Copy link
Copy Markdown
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

# frozen_string_literal: true

require 'base64'
require 'bigdecimal'
Copy link
Copy Markdown
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
Copy Markdown
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.

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
Copy Markdown
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
Copy Markdown
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.

attr_accessor :client

# @return [Hash] The hash of request parameters.
# @return [Hash, Struct] The request parameters as a Hash or a Struct.
Copy link
Copy Markdown
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
Copy Markdown
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.

return if values.nil?

next unless ref.shape.member?(k)
type = @convert_structures ? ref.shape.type.new : values
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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 thread gems/smithy-schema/lib/smithy-schema/shapes.rb
Comment on lines +88 to +91
'traits' => {
'smithy.api#required' => {},
'smithy.api#clientOptional' => {}
}
Copy link
Copy Markdown
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
Copy Markdown
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