Skip to content
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

Allow jsonName trait on union members #1153

Merged
merged 1 commit into from
Mar 23, 2022
Merged
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
11 changes: 8 additions & 3 deletions docs/source/1.0/spec/core/protocol-traits.rst
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,11 @@ support configuration settings.

Summary
Allows a serialized object property name in a JSON document to differ from
a structure member name used in the model.
a structure or union member name used in the model.
Trait selector
``structure > member``
``:is(structure, union) > member``

*Any structure member*
*Any structure or union member*
Value type
``string``

Expand Down Expand Up @@ -203,6 +203,11 @@ following document:
"bar": "def"
}

.. note::

No two members of the same structure or union can use the
same case-sensitive ``@jsonName``.


.. smithy-trait:: smithy.api#mediaType
.. _mediaType-trait:
Expand Down
153 changes: 153 additions & 0 deletions smithy-aws-protocol-tests/model/restJson1/unions.smithy
Original file line number Diff line number Diff line change
Expand Up @@ -512,3 +512,156 @@ apply PostPlayerAction @httpResponseTests([
}
}
])


/// This operation defines a union that uses jsonName on some members.
@http(uri: "/PostUnionWithJsonName", method: "POST")
operation PostUnionWithJsonName {
input: PostUnionWithJsonNameInput,
output: PostUnionWithJsonNameOutput
}

@input
structure PostUnionWithJsonNameInput {
@required
value: UnionWithJsonName
}

@output
structure PostUnionWithJsonNameOutput {
@required
value: UnionWithJsonName
}

union UnionWithJsonName {
@jsonName("FOO")
foo: String,

bar: String,

@jsonName("_baz")
baz: String
}

apply PostUnionWithJsonName @httpRequestTests([
{
id: "PostUnionWithJsonNameRequest1",
documentation: "Tests that jsonName works with union members.",
protocol: restJson1,
method: "POST",
uri: "/PostUnionWithJsonName",
body: """
{
"value": {
"FOO": "hi"
}
}""",
bodyMediaType: "application/json",
headers: {"Content-Type": "application/json"},
params: {
value: {
foo: "hi"
}
}
},
{
id: "PostUnionWithJsonNameRequest2",
documentation: "Tests that jsonName works with union members.",
protocol: restJson1,
method: "POST",
uri: "/PostUnionWithJsonName",
body: """
{
"value": {
"_baz": "hi"
}
}""",
bodyMediaType: "application/json",
headers: {"Content-Type": "application/json"},
params: {
value: {
baz: "hi"
}
}
},
{
id: "PostUnionWithJsonNameRequest3",
documentation: "Tests that jsonName works with union members.",
protocol: restJson1,
method: "POST",
uri: "/PostUnionWithJsonName",
body: """
{
"value": {
"bar": "hi"
}
}""",
bodyMediaType: "application/json",
headers: {"Content-Type": "application/json"},
params: {
value: {
bar: "hi"
}
}
}
])

apply PostUnionWithJsonName @httpResponseTests([
{
id: "PostUnionWithJsonNameResponse1",
documentation: "Tests that jsonName works with union members.",
protocol: restJson1,
code: 200,
body: """
{
"value": {
"FOO": "hi"
}
}""",
bodyMediaType: "application/json",
headers: {"Content-Type": "application/json"},
params: {
value: {
foo: "hi"
}
}
},
{
id: "PostUnionWithJsonNameResponse2",
documentation: "Tests that jsonName works with union members.",
protocol: restJson1,
code: 200,
body: """
{
"value": {
"_baz": "hi"
}
}""",
bodyMediaType: "application/json",
headers: {"Content-Type": "application/json"},
params: {
value: {
baz: "hi"
}
}
},
{
id: "PostUnionWithJsonNameResponse3",
documentation: "Tests that jsonName works with union members.",
protocol: restJson1,
code: 200,
body: """
{
"value": {
"bar": "hi"
}
}""",
bodyMediaType: "application/json",
headers: {"Content-Type": "application/json"},
params: {
value: {
bar: "hi"
}
}
}
])
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.smithy.model.validation.validators;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Collectors;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.traits.JsonNameTrait;
import software.amazon.smithy.model.validation.AbstractValidator;
import software.amazon.smithy.model.validation.ValidationEvent;

public final class JsonNameValidator extends AbstractValidator {
@Override
public List<ValidationEvent> validate(Model model) {
List<ValidationEvent> events = new ArrayList<>();
Set<ShapeId> visitedContainers = new HashSet<>();

// Find every member marked with a jsonName trait. The containing shapes of these members are
// the only structure/union shapes that need to be validated.
for (MemberShape member : model.getMemberShapesWithTrait(JsonNameTrait.class)) {
// If the container hasn't been visited yet, then validate it's members.
if (visitedContainers.add(member.getContainer())) {
validateMembersOfContainer(model.expectShape(member.getContainer()), events);
}
}
return events;
}

private void validateMembersOfContainer(Shape container, List<ValidationEvent> events) {
Map<String, Set<MemberShape>> memberMappings = new TreeMap<>();
for (MemberShape m : container.members()) {
String jsonName = m.getTrait(JsonNameTrait.class)
.map(JsonNameTrait::getValue)
.orElseGet(m::getMemberName);
memberMappings.computeIfAbsent(jsonName, n -> new TreeSet<>()).add(m);
}

for (Map.Entry<String, Set<MemberShape>> entry : memberMappings.entrySet()) {
if (entry.getValue().size() > 1) {
events.add(error(container, String.format(
"This shape contains members with conflicting JSON names that resolve to '%s': %s",
entry.getKey(),
entry.getValue().stream().map(MemberShape::getMemberName).collect(Collectors.joining(", ")))));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ software.amazon.smithy.model.validation.validators.HttpQueryParamsTraitValidator
software.amazon.smithy.model.validation.validators.HttpQueryTraitValidator
software.amazon.smithy.model.validation.validators.HttpResponseCodeSemanticsValidator
software.amazon.smithy.model.validation.validators.HttpUriConflictValidator
software.amazon.smithy.model.validation.validators.JsonNameValidator
software.amazon.smithy.model.validation.validators.LengthTraitValidator
software.amazon.smithy.model.validation.validators.MediaTypeValidator
software.amazon.smithy.model.validation.validators.NoInlineDocumentSupportValidator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ structure internal {}

/// The jsonName trait allows a serialized object property name to differ
/// from a structure member name used in the model.
@trait(selector: "structure > member")
@trait(selector: ":is(structure, union) > member")
@tags(["diff.error.const"])
string jsonName

Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
[ERROR] ns.foo#B: Trait `jsonName` cannot be applied to `ns.foo#B`. This trait may only be applied to shapes that match the following selector: structure > member | TraitTarget
[ERROR] ns.foo#B: Trait `jsonName` cannot be applied to `ns.foo#B`. This trait may only be applied to shapes that match the following selector: :is(structure, union) | TraitTarget
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[ERROR] ns.foo#A: This shape contains members with conflicting JSON names that resolve to 'b': a, b | JsonName
[ERROR] ns.foo#B: This shape contains members with conflicting JSON names that resolve to 'b': a, b | JsonName
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"smithy": "1.0",
"shapes": {
"ns.foo#A": {
"type": "structure",
"members": {
"a": {
"target": "smithy.api#String",
"traits": {
"smithy.api#jsonName": "b"
}
},
"b": {
"target": "smithy.api#String"
},
"c": {
"target": "smithy.api#String",
"traits": {
"smithy.api#jsonName": "a"
}
}
}
},
"ns.foo#B": {
"type": "union",
"members": {
"a": {
"target": "smithy.api#String",
"traits": {
"smithy.api#jsonName": "b"
}
},
"b": {
"target": "smithy.api#String"
},
"c": {
"target": "smithy.api#String",
"traits": {
"smithy.api#jsonName": "a"
}
}
}
}
}
}