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

Service renames #734

Merged
merged 7 commits into from
Mar 12, 2021
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
5 changes: 5 additions & 0 deletions docs/source/1.0/guides/converting-to-openapi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ service (``string``)
}
}

.. note::

Any :ref:`rename <service-closure>` defined in the given service
affects the generated schema names when converting to OpenAPI.

.. _generate-openapi-setting-protocol:

protocol (``string``)
Expand Down
15 changes: 14 additions & 1 deletion docs/source/1.0/guides/evolving-models.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@ customers.
:backlinks: none


Updating services
=================

The following changes to operation shapes are backward-compatible:

#. Adding operations.
#. Adding resources.

The following changes are not backward-compatible:

#. Removing a resource or operation.
#. Renaming a shape that was already part of the service.


Updating operations
===================

Expand All @@ -27,7 +41,6 @@ The following changes are not backward-compatible:
#. Removing or renaming a resource or operation.
#. Removing an operation from a service or resource.
#. Removing a resource from a service.
#. Changing the parent of a resource.
#. Changing an operation from referencing an input/output structure to no
longer referencing an input/output structure.
#. Renaming an error that is referenced by an operation.
Expand Down
15 changes: 13 additions & 2 deletions docs/source/1.0/spec/core/json-ast.rst
Original file line number Diff line number Diff line change
Expand Up @@ -400,8 +400,12 @@ shapes defined in JSON support the same properties as the Smithy IDL.
- Binds a list of resources to the service. Each reference MUST target
a resource.
* - traits
- Map\<:ref:`shape ID <shape-id>`, trait value>
- map of :ref:`shape ID <shape-id>` to trait values
- Traits to apply to the service
* - rename
- map of :ref:`shape ID <shape-id>` to ``string`` :token:`identifier`
- Disambiguates shape name conflicts in the
:ref:`service closure <service-closure>`.

.. code-block:: json

Expand All @@ -420,7 +424,14 @@ shapes defined in JSON support the same properties as the Smithy IDL.
{
"target": "smithy.example#SomeResource"
}
]
],
"traits": {
"smithy.api#documentation": "Documentation for the service"
},
"rename": {
"smithy.example#Widget": "SmithyWidget",
"foo.example#Widget": "FooWidget"
}
}
}
}
Expand Down
115 changes: 113 additions & 2 deletions docs/source/1.0/spec/core/model.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1219,6 +1219,27 @@ The service shape supports the following properties:
- Binds a set of ``resource`` shapes to the service. Each element in
the given list MUST be a valid :ref:`shape ID <shape-id>` that targets
a :ref:`resource <resource>` shape.
* - rename
- map of :ref:`shape ID <shape-id>` to ``string``
- Disambiguates shape name conflicts in the
:ref:`service closure <service-closure>`. Map keys are shape IDs
contained in the service, and map values are the disambiguated shape
names to use in the context of the service. Each given shape ID MUST
reference a shape contained in the closure of the service. Each given
map value MUST match the :token:`identifier` production used for
shape IDs. Renaming a shape *does not* give the shape a new shape ID.

* No renamed shape name can case-insensitively match any other renamed
shape name or the name of a non-renamed shape contained in the
service.
* Member shapes MAY NOT be renamed.
* Resource, operation, and shapes marked with the :ref:`error-trait`
MAY NOT be renamed. Renaming shapes is intended for incidental naming
conflicts, not for renaming the fundamental concepts of a service.
* Shapes from other namespaces marked as :ref:`private <private-trait>`
MAY be renamed.
* A rename MUST use a name that is case-sensitively different from the
mtdowling marked this conversation as resolved.
Show resolved Hide resolved
original shape ID name.

The following example defines a service with no operations or resources.

Expand Down Expand Up @@ -1349,7 +1370,8 @@ through resources, operations, and members.

With some exceptions, the shapes that are referenced in the *closure*
of a service MUST have case-insensitively unique names regardless of
their namespace.
their namespace, and conflicts MUST be disambiguated using the
``rename`` property of a service.

By requiring unique names within a service, each service forms a
`ubiquitous language`_, making it easier for developers to understand the
Expand All @@ -1361,12 +1383,100 @@ case-insensitively because many model transformations (like code generation)
change the casing and inflection of shape names to make artifacts more
idiomatic.

.. rubric:: Shape types allowed to conflict in a closure

:ref:`Simple types <simple-types>` and :ref:`lists <list>` or
:ref:`sets <set>` of compatible simple types are allowed to conflict because
a conflict for these type would rarely have an impact on generated artifacts.
These kinds of conflicts are only allowed if both conflicting shapes are the
same type and have the exact same traits. In the case of a list or set, a
conflict is only allowed if both types target compatible shapes.
conflict is only allowed if the members of the conflicting shapes target
compatible shapes.

.. rubric:: Disambiguating shapes with ``rename``

The ``rename`` property of a service is used to disambiguate conflicting
shape names found in the closure of a service. The ``rename`` property is
essentially a `context map`_ used to ensure that the service still presents
a ubiquitous language despite bringing together shapes from multiple
namespaces.

.. note::

Renames SHOULD be used sparingly. Renaming shapes is something typically
only needed when aggregating models from multiple independent teams into
a single service.

The following example defines a service that contains two shapes named
"Widget" in its closure. The ``rename`` property is used to disambiguate
the conflicting shapes.

.. tabs::

.. code-tab:: smithy

namespace smithy.example

service MyService {
version: "2017-02-11",
operations: [GetSomething],
rename: {
"foo.example#Widget": "FooWidget"
}
}

operation GetSomething {
output: GetSomethingOutput,
}

structure GetSomethingOutput {
widget1: Widget,
fooWidget: foo.example#Widget,
}

structure Widget {}

.. code-tab:: json

{
"smithy": "1.0",
"shapes": {
"smithy.example#MyService": {
"type": "service",
"version": "2017-02-11",
"operations": [
{
"target": "smithy.example#GetSomething"
}
],
"rename": {
"foo.example#Widget": "FooWidget"
}
},
"smithy.example#GetSomething": {
"type": "operation",
"output": {
"target": "smithy.example#GetSomethingOutput"
}
},
"smithy.example#GetSomethingOutput": {
"type": "structure",
"members": {
"widget1": {
"target": "smithy.example#Widget"
},
"fooWidget": {
"target": "foo.example#Widget"
}
}
},
"smithy.example#Widget": {
"type": "structure"
}
}
}

.. rubric:: Resources and operations can be bound once

An operation or resource MUST NOT be bound to multiple shapes within the
closure of a service. This constraint allows services to discern between
Expand Down Expand Up @@ -2766,3 +2876,4 @@ after adding a member to the ``foo`` trait:

.. _tagged union data structure: https://en.wikipedia.org/wiki/Tagged_union
.. _ubiquitous language: https://martinfowler.com/bliki/UbiquitousLanguage.html
.. _context map: https://martinfowler.com/bliki/BoundedContext.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ public final class CfnConfig extends JsonSchemaConfig {
private Map<ShapeId, Map<String, Node>> jsonAdd = Collections.emptyMap();
private String organizationName;
private String serviceName;
private ShapeId service;
private List<String> sourceDocs = ListUtils.of(
"Source Url", "SourceUrl", "Source", "Source Code");

Expand Down Expand Up @@ -232,21 +231,6 @@ public void setServiceName(String serviceName) {
this.serviceName = serviceName;
}

public ShapeId getService() {
return service;
}

/**
* Sets the service shape ID to convert the resources of.
*
* <p>For example, smithy.example#Weather.
*
* @param service the Smithy service shape ID to convert the resources of.
*/
public void setService(ShapeId service) {
this.service = service;
}

public List<String> getSourceDocs() {
return sourceDocs;
}
Expand Down
4 changes: 4 additions & 0 deletions smithy-aws-protocol-tests/model/restJson1/main.smithy
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ use smithy.test#httpResponseTests
@restJson1
service RestJson {
version: "2019-12-16",
// Ensure that generators are able to handle renames.
rename: {
"aws.protocoltests.restjson.nested#GreetingStruct": "RenamedGreeting",
},
operations: [
// Basic input and output tests
NoInputAndNoOutput,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
$version: "1.0"

namespace aws.protocoltests.restjson.nested

// Note that this conflicts with the shared-types GreetingStruct
// and needs to be renamed if used as part of a service closure.
structure GreetingStruct {
salutation: String,
}
28 changes: 28 additions & 0 deletions smithy-aws-protocol-tests/model/restJson1/unions.smithy
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ union MyUnion {
listValue: StringList,
mapValue: StringMap,
structureValue: GreetingStruct,

// Note that this uses a conflicting structure name with
// GreetingStruct, so it must be renamed in the service.
renamedStructureValue: aws.protocoltests.restjson.nested#GreetingStruct,
}

apply JsonUnions @httpRequestTests([
Expand Down Expand Up @@ -230,6 +234,30 @@ apply JsonUnions @httpRequestTests([
}
}
},
{
id: "RestJsonSerializeRenamedStructureUnionValue",
documentation: "Serializes a renamed structure union value",
protocol: restJson1,
method: "PUT",
uri: "/JsonUnions",
body: """
{
"contents": {
"renamedStructureValue": {
"salutation": "hello!"
}
}
}""",
bodyMediaType: "application/json",
headers: {"Content-Type": "application/json"},
params: {
contents: {
renamedStructureValue: {
salutation: "hello!",
}
}
}
},
])

apply JsonUnions @httpResponseTests([
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2021 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.diff.evaluators;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import software.amazon.smithy.diff.Differences;
import software.amazon.smithy.model.neighbor.Walker;
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.validation.ValidationEvent;

/**
* Creates an ERROR event when a shape is renamed that was already
* part of the service, when a rename changes for a shape, or when
* a rename is removed for a shape.
*/
public final class ServiceRename extends AbstractDiffEvaluator {
@Override
public List<ValidationEvent> evaluate(Differences differences) {
Walker oldWalker = new Walker(differences.getOldModel());

return differences.changedShapes(ServiceShape.class)
.flatMap(diff -> {
ServiceShape oldShape = diff.getOldShape();
ServiceShape newShape = diff.getNewShape();
if (oldShape.getRename().equals(newShape.getRename())) {
return Stream.empty();
}

// Look for the removal or changing of old renames.
List<ValidationEvent> events = new ArrayList<>();
for (Map.Entry<ShapeId, String> old : oldShape.getRename().entrySet()) {
String newValue = newShape.getRename().get(old.getKey());
if (newValue == null) {
events.add(error(newShape, String.format(
"Service rename of `%s` to `%s` was removed",
old.getKey(), old.getValue())));
} else if (!old.getValue().equals(newValue)) {
events.add(error(newShape, String.format(
"Service rename of `%s` was changed from `%s` to `%s`",
old.getKey(), old.getValue(), newValue)));
}
}

// Look for the addition of new renames to shapes already in the closure.
Set<ShapeId> oldClosure = oldWalker.walkShapeIds(oldShape);
for (Map.Entry<ShapeId, String> newEntry : newShape.getRename().entrySet()) {
if (!oldShape.getRename().containsKey(newEntry.getKey())) {
if (oldClosure.contains(newEntry.getKey())) {
events.add(error(newShape, String.format(
"Service rename of `%s` to `%s` was added to an old shape",
newEntry.getKey(), newEntry.getValue())));
}
}
}

return events.stream();
})
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ software.amazon.smithy.diff.evaluators.RemovedOperationInput
software.amazon.smithy.diff.evaluators.RemovedOperationOutput
software.amazon.smithy.diff.evaluators.RemovedShape
software.amazon.smithy.diff.evaluators.RemovedTraitDefinition
software.amazon.smithy.diff.evaluators.ServiceRename
Loading