Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 12 additions & 3 deletions gems/smithy-cbor/lib/smithy-cbor/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,20 @@ def map(shape, values)

def structure(shape, values)
return if values.nil?

shape.target.members.each_with_object({}) do |(member_name, member_shape), data|
value = values[member_name]
# Mirrors smithy-json's guard for the same reason (see that builder's
# `structure` method): unreachable today since `build` already
# short-circuits `Prelude::Unit` above, but keeps both builders'
# fallback behavior identical if that ever changes. Flagged for
# review, not verified against a real non-hash input reaching here.
return {} unless values.respond_to?(:each_pair)

members = shape.target.members
values.each_pair.with_object({}) do |(member_name, value), data|
next if value.nil?

member_shape = members[member_name]
next unless member_shape

data[member_shape.location_name] = build_shape(member_shape, value)
end
end
Expand Down
10 changes: 7 additions & 3 deletions gems/smithy-cbor/lib/smithy-cbor/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,13 @@ def map(shape, values, result = nil)

def structure(shape, values, result = nil)
result = shape.target.type.new if result.nil?
shape.target.members.each do |member_name, member_shape|
value = values[member_shape.location_name]
result[member_name] = parse_shape(member_shape, value) unless value.nil?
values.each do |wire_name, value|
next if value.nil?

member_name, member_shape = shape.target.member_by_wire_name(wire_name)
next unless member_shape

result[member_name] = parse_shape(member_shape, value)
end
result
end
Expand Down
10 changes: 10 additions & 0 deletions gems/smithy-cbor/spec/smithy-cbor/builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ module Cbor
bytes = subject.build(structure_shape, data.merge(structure: data))
expect(Cbor.decode(bytes)).to eq(expected.merge('structure' => expected))
end

it 'builds only the members present on a sparse input, iterating values not declared members' do
bytes = subject.build(structure_shape, { string: 'string', integer: 1 })
expect(Cbor.decode(bytes)).to eq('string' => 'string', 'integer' => 1)
end

it 'skips keys in the input that are not declared members of the shape' do
bytes = subject.build(structure_shape, { string: 'string', not_a_real_member: 'ignored' })
expect(Cbor.decode(bytes)).to eq('string' => 'string')
end
end

context 'unions' do
Expand Down
12 changes: 12 additions & 0 deletions gems/smithy-cbor/spec/smithy-cbor/parser_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,18 @@ module Cbor
bytes = Cbor.encode(data)
expect(subject.parse(structure_shape, bytes).to_h).to eq(expected)
end

it 'ignores wire keys that are not members' do
data = { 'string' => 'string', 'notAMember' => 'ignored' }
bytes = Cbor.encode(data)
expect(subject.parse(structure_shape, bytes).to_h).to eq(string: 'string')
end

it 'skips members whose wire value is nil' do
data = { 'string' => nil }
bytes = Cbor.encode(data)
expect(subject.parse(structure_shape, bytes).to_h).to eq({})
end
end

context 'unions' do
Expand Down
12 changes: 9 additions & 3 deletions gems/smithy-json/lib/smithy-json/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,16 @@ def map(shape, values)

def structure(shape, values)
return if values.nil?
return {} unless values.respond_to?(:each_pair)

shape.target.members.each_with_object({}) do |(member_name, member_shape), data|
value = values[member_name]
data[location_name(member_shape)] = build_shape(member_shape, value) unless value.nil?
members = shape.target.members
values.each_pair.with_object({}) do |(member_name, value), data|
next if value.nil?

member_shape = members[member_name]
next unless member_shape

data[location_name(member_shape)] = build_shape(member_shape, value)
end
end

Expand Down
10 changes: 7 additions & 3 deletions gems/smithy-json/lib/smithy-json/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,13 @@ def structure(shape, values, result = nil)
return if values.nil?

result = shape.target.type.new if result.nil?
shape.target.members.each do |member_name, member_shape|
value = values[location_name(member_shape)]
result[member_name] = parse_shape(member_shape, value) unless value.nil?
values.each do |wire_name, value|
next if value.nil?

member_name, member_shape = shape.target.member_by_wire_name(wire_name)
next unless member_shape

result[member_name] = parse_shape(member_shape, value)
end
result
end
Expand Down
12 changes: 12 additions & 0 deletions gems/smithy-json/spec/smithy-json/builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ module Json
bytes = subject.build(structure_shape, data)
expect(Json.load(bytes)).to eq('NewString' => 'string')
end

it 'builds only the members present on a sparse input, iterating values not declared members' do
data = { string: 'string', integer: 1 }
bytes = subject.build(structure_shape, data)
expect(Json.load(bytes)).to eq('string' => 'string', 'integer' => 1)
end

it 'skips keys in the input that are not declared members of the shape' do
data = { string: 'string', not_a_real_member: 'ignored' }
bytes = subject.build(structure_shape, data)
expect(Json.load(bytes)).to eq('string' => 'string')
end
end

context 'unions' do
Expand Down
22 changes: 22 additions & 0 deletions gems/smithy-json/spec/smithy-json/parser_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,28 @@ module Json
bytes = Json.dump(data)
expect(subject.parse(structure_shape, bytes).to_h).to eq(string: 'string')
end

it 'resolves members keyed by jsonName even without the json_name option' do
shapes['smithy.ruby.tests#Structure']['members']['string'] = {
'target' => 'smithy.api#String',
'traits' => { 'smithy.api#jsonName' => 'NewString' }
}
data = { 'NewString' => 'string' }
bytes = Json.dump(data)
expect(subject.parse(structure_shape, bytes).to_h).to eq(string: 'string')
end

it 'ignores wire keys that are not members' do
data = { 'string' => 'string', 'notAMember' => 'ignored' }
bytes = Json.dump(data)
expect(subject.parse(structure_shape, bytes).to_h).to eq(string: 'string')
end

it 'skips members whose wire value is nil' do
data = { 'string' => nil }
bytes = Json.dump(data)
expect(subject.parse(structure_shape, bytes).to_h).to eq({})
end
end

context 'unions' do
Expand Down
30 changes: 30 additions & 0 deletions gems/smithy-schema/lib/smithy-schema/shapes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ class StructureShape < Shape
def initialize(options = {})
super
@members = {}
@members_by_wire_name = {}
end

# @return [Hash<Symbol, MemberShape>]
Expand All @@ -246,6 +247,10 @@ def initialize(options = {})
# @return [MemberShape]
def add_member(name, member_shape)
@members[name] = member_shape
register_wire_name(member_shape.location_name, name, member_shape)
json_name = member_shape.traits['smithy.api#jsonName']
register_wire_name(json_name, name, member_shape) if json_name && json_name != member_shape.location_name
member_shape
end

# @param [Symbol] name
Expand All @@ -259,6 +264,31 @@ def member?(name)
def member(name)
@members[name]
end

# Resolves a member from its on-the-wire name. Members are registered
# under both their +location_name+ and their +jsonName+ (when present),
# so a response keyed by either flavor resolves without the shape
# carrying a protocol-specific winner. Built once at load time.
# @param [String] wire_name
# @return [[Symbol, MemberShape], nil] +[member_name, member_shape]+ or nil
def member_by_wire_name(wire_name)
@members_by_wire_name[wire_name]
end

private

def register_wire_name(wire_name, name, member_shape)
return if wire_name.nil?

existing = @members_by_wire_name[wire_name]
if existing && existing[0] != name
raise ArgumentError,
"wire name collision on #{@id.inspect}: #{wire_name.inspect} " \
"maps to both #{existing[0].inspect} and #{name.inspect}"
end

@members_by_wire_name[wire_name] = [name, member_shape]
end
end

# Represents a Timestamp shape.
Expand Down
4 changes: 4 additions & 0 deletions gems/smithy-schema/sig/smithy-schema/shapes.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ module Smithy
def add_member: (Symbol, MemberShape) -> MemberShape
def member?: (Symbol?) -> bool
def member: (Symbol) -> MemberShape?
def member_by_wire_name: (String) -> [Symbol, MemberShape]?

private
def register_wire_name: (String?, Symbol, MemberShape) -> void
end

class TimestampShape < Shape
Expand Down
55 changes: 55 additions & 0 deletions gems/smithy-schema/spec/smithy-schema/shapes_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,61 @@ module Shapes
expect(subject.member(:foo)).to be_kind_of(MemberShape)
end
end

describe '#member_by_wire_name' do
it 'resolves a member by its location name' do
member_shape = MemberShape.new(target: StringShape.new, location_name: 'foo')
subject.add_member(:foo, member_shape)
expect(subject.member_by_wire_name('foo')).to eq([:foo, member_shape])
end

it 'also resolves a member by its jsonName when present' do
member_shape = MemberShape.new(
target: StringShape.new,
location_name: 'foo',
traits: { 'smithy.api#jsonName' => 'Foo' }
)
subject.add_member(:foo, member_shape)
expect(subject.member_by_wire_name('foo')).to eq([:foo, member_shape])
expect(subject.member_by_wire_name('Foo')).to eq([:foo, member_shape])
end

it 'registers a single entry when jsonName equals the location name' do
member_shape = MemberShape.new(
target: StringShape.new,
location_name: 'foo',
traits: { 'smithy.api#jsonName' => 'foo' }
)
subject.add_member(:foo, member_shape)
expect(subject.member_by_wire_name('foo')).to eq([:foo, member_shape])
end

it 'returns nil for an unknown wire name' do
subject.add_member(:foo, MemberShape.new(target: StringShape.new, location_name: 'foo'))
expect(subject.member_by_wire_name('unknown')).to be_nil
end

it 'raises when two members share a location name' do
subject.add_member(:foo, MemberShape.new(target: StringShape.new, location_name: 'shared'))
expect do
subject.add_member(:bar, MemberShape.new(target: StringShape.new, location_name: 'shared'))
end.to raise_error(ArgumentError, /wire name collision/)
end

it 'raises when a jsonName collides with another member location name' do
subject.add_member(:foo, MemberShape.new(target: StringShape.new, location_name: 'shared'))
expect do
subject.add_member(
:bar,
MemberShape.new(
target: StringShape.new,
location_name: 'bar',
traits: { 'smithy.api#jsonName' => 'shared' }
)
)
end.to raise_error(ArgumentError, /wire name collision/)
end
end
end

describe TimestampShape do
Expand Down
Loading