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

Add flattenNamespaces transformer #572

Merged
merged 3 commits into from
Sep 29, 2020
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
59 changes: 59 additions & 0 deletions docs/source/1.0/guides/building-models/build-config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,65 @@ key is not in the provided ``keys`` list.
}
}

.. _flattenNamespaces:

flattenNamespaces
srchase marked this conversation as resolved.
Show resolved Hide resolved
-----------------

Flattens namespaces of any shapes connected to a service into a target
namespace. Shapes not connected to a service will not be flattened.

.. list-table::
:header-rows: 1
:widths: 10 20 70

* - Property
- Type
- Description
* - namespace
- ``string``
- **REQUIRED** The target namespace.
* - service
- ``shapeId``
- **REQUIRED** The service to be flattened. All shapes within this
:ref:`service closure <service-closure>` will be replaced with equivalent
shapes in the target namespace.
* - includeTagged
- ``[string]``
- The set of tags that, if found on a shape not connected to the service,
mtdowling marked this conversation as resolved.
Show resolved Hide resolved
forces the shape to have its namespace flattened into the target
namespace. When additional shapes are included, the shapes are replaced
srchase marked this conversation as resolved.
Show resolved Hide resolved
entirely, along with any references to the shapes which may exist within
separate :ref:`service closures <service-closure>`.

The following example will flatten the namespaces of the shapes connected to
the ``ns.bar#MyService`` service into the target namespace, ``ns.foo``. Shapes
tagged with ``baz`` or ``qux`` will also be flattened into the ``ns.foo``
namespace, so long as they don't conflict with a shape within the :ref:`service closure <service-closure>`.

.. tabs::

.. code-tab:: json

{
"version": "1.0",
"projections": {
"exampleProjection": {
"transforms": [
{
"name": "flattenNamespaces",
"args": {
"namespace": "ns.foo",
"service": "ns.bar#MyService",
"includeTagged": ["baz", "qux"]
}
}
]
}
}
}


.. _removeTraitDefinitions-transform:

removeTraitDefinitions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*
* Copyright 2020 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.build.transforms;

import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import software.amazon.smithy.build.SmithyBuildException;
import software.amazon.smithy.build.TransformContext;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.knowledge.NeighborProviderIndex;
import software.amazon.smithy.model.loader.Prelude;
import software.amazon.smithy.model.neighbor.Walker;
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.transform.ModelTransformer;
import software.amazon.smithy.utils.FunctionalUtils;
import software.amazon.smithy.utils.Pair;

/**
* {@code flattenNamespaces} updates a model by flattening the namespaces of
* shapes connected to a service into a single, target namespace. When
* configuring the transformer, a service and target namespace must be
* specified. Optionally, tags can be specified for including any additional
* shapes that should be flattened into the the target namespace. Any shape
* from outside the service closure that is included via the application of a
* tag will not be included if it conflicts with a shape in the service closure.
*/
public final class FlattenNamespaces extends ConfigurableProjectionTransformer<FlattenNamespaces.Config> {

/**
* {@code removeTraitShapes} configuration settings.
*/
public static final class Config {

private String namespace;
private ShapeId service;
private Set<String> tags = Collections.emptySet();

/**
* Sets the target namespace that existing namespaces will be flattened
* into.
*
* @param namespace The target namespace to use in the model.
*/
public void setNamespace(String namespace) {
this.namespace = namespace;
}

/**
* Gets the target namespace that existing namespaces will be flattened
* into.
*
* @return the target namespace to be used in the model.
*/
public String getNamespace() {
return namespace;
}

/**
* Sets the service ShapeId that will be flattened into the target
* namespace.
*
* @param service The ID of the service.
*/
public void setService(ShapeId service) {
this.service = service;
}

/**
* @return Gets the service shape ID of the service that will have
* its shape namespaces updated.
*/
public ShapeId getService() {
return service;
}

/**
* Sets the set of tags that are retained in the model.
*
* @param tags The tags to retain in the model.
*/
public void setIncludeTagged(Set<String> tags) {
this.tags = tags;
}

/**
* Gets the set of tags that are retained in the model.
*
* @return Returns the tags to retain.
*/
public Set<String> getIncludeTagged() {
return tags;
}
}

@Override
public Class<Config> getConfigType() {
return Config.class;
}

@Override
protected Model transformWithConfig(TransformContext context, Config config) {
if (config.getService() == null || config.getNamespace() == null) {
throw new SmithyBuildException(
"'namespace' and 'service' properties must be set on flattenNamespace transformer.");
}
Model model = context.getModel();
Map<ShapeId, ShapeId> shapesToRename = getRenamedShapes(config, model);
return ModelTransformer.create().renameShapes(model, shapesToRename);
}

@Override
public String getName() {
return "flattenNamespaces";
}

private Map<ShapeId, ShapeId> getRenamedShapes(Config config, Model model) {
if (!model.getShape(config.getService()).isPresent()) {
throw new SmithyBuildException("Configured service, " + config.getService()
+ ", not found in model when performing flattenNamespaces transform.");
}

Map<ShapeId, ShapeId> shapesToRename = getRenamedShapesConnectedToService(config, model);
Set<ShapeId> taggedShapesToInclude = getTaggedShapesToInclude(config.getIncludeTagged(), model);

for (ShapeId id : taggedShapesToInclude) {
ShapeId updatedShapeId = updateNamespace(id, config.getNamespace());
// If new shape ID already exists in map of shapes to rename, skip
// including the additional shape to avoid a conflict.
if (!shapesToRename.containsValue(updatedShapeId)) {
shapesToRename.put(id, updatedShapeId);
}
}

return shapesToRename;
}

private ShapeId updateNamespace(ShapeId shapeId, String namespace) {
if (shapeId.getMember().isPresent()) {
return ShapeId.fromParts(namespace, shapeId.getName(), shapeId.getMember().get());
}
return ShapeId.fromParts(namespace, shapeId.getName());
}

private Map<ShapeId, ShapeId> getRenamedShapesConnectedToService(Config config, Model model) {
Walker shapeWalker = new Walker(NeighborProviderIndex.of(model).getProvider());
ServiceShape service = model.expectShape(config.getService(), ServiceShape.class);
return shapeWalker.walkShapes(service).stream()
.filter(FunctionalUtils.not(Prelude::isPreludeShape))
.map(shape -> Pair.of(shape.getId(), updateNamespace(shape.getId(), config.getNamespace())))
.collect(Collectors.toMap(Pair::getLeft, Pair::getRight));
}

private Set<ShapeId> getTaggedShapesToInclude(Set<String> tags, Model model) {
return model.shapes()
.filter(FunctionalUtils.not(Prelude::isPreludeShape))
.filter(shape -> isTagged(tags, shape))
.map(Shape::getId)
.collect(Collectors.toSet());
}

private boolean isTagged(Set<String> tags, Shape shape) {
return shape.getTags().stream().anyMatch(tags::contains);
}
}
Loading