All Downloads are FREE. Search and download functionalities are using the official Maven repository.

software.amazon.smithy.model.transform.ModelTransformer Maven / Gradle / Ivy

Go to download

This module provides the core implementation of loading, validating, traversing, mutating, and serializing a Smithy model.

There is a newer version: 1.54.0
Show newest version
/*
 * Copyright 2019 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.transform;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.loader.ModelAssembler;
import software.amazon.smithy.model.neighbor.UnreferencedShapes;
import software.amazon.smithy.model.neighbor.UnreferencedTraitDefinitions;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.shapes.MemberShape;
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.shapes.ShapeType;
import software.amazon.smithy.model.traits.EnumTrait;
import software.amazon.smithy.model.traits.Trait;
import software.amazon.smithy.model.traits.TraitDefinition;
import software.amazon.smithy.model.traits.synthetic.OriginalShapeIdTrait;
import software.amazon.smithy.utils.FunctionalUtils;
import software.amazon.smithy.utils.ListUtils;

/**
 * Class used to transform {@link Model}s.
 */
public final class ModelTransformer {
    private final List plugins;

    private ModelTransformer(List plugins) {
        this.plugins = ListUtils.copyOf(plugins);
    }

    /**
     * Creates a ModelTransformer using ModelTransformerPlugin instances
     * discovered using the {@code com.software.smithy.transform} class
     * loader and any modules found in the module path.
     *
     * @return Returns the created ModelTransformer.
     */
    public static ModelTransformer create() {
        return DefaultHolder.INSTANCE;
    }

    // Lazy initialization holder class idiom
    private static class DefaultHolder {
        static final ModelTransformer INSTANCE = createWithServiceProviders(ModelTransformer.class.getClassLoader());
    }

    private static ModelTransformer createWithServiceLoader(ServiceLoader serviceLoader) {
        List plugins = new ArrayList<>();
        serviceLoader.forEach(plugins::add);
        return createWithPlugins(plugins);
    }

    /**
     * Creates a ModelTransformer using a list of ModelTransformer plugins.
     *
     * @param plugins Plugins to use with the transformer.
     * @return Returns the created ModelTransformer.
     */
    public static ModelTransformer createWithPlugins(List plugins) {
        return new ModelTransformer(plugins);
    }

    /**
     * Creates a ModelTransformer that finds {@link ModelTransformerPlugin}
     * service providers using the given {@link ClassLoader}.
     *
     * @param classLoader ClassLoader used to find ModelTransformerPlugin instances.
     * @return Returns the created ModelTransformer.
     */
    public static ModelTransformer createWithServiceProviders(ClassLoader classLoader) {
        return createWithServiceLoader(ServiceLoader.load(ModelTransformerPlugin.class, classLoader));
    }

    /**
     * Adds or replaces shapes into the model while ensuring that the model
     * is in a consistent state.
     *
     * @param model Model to transform.
     * @param shapes Shapes to add or replace in the model.base.
     * @return Returns the transformed model.base.
     */
    public Model replaceShapes(Model model, Collection shapes) {
        if (shapes.isEmpty()) {
            return model;
        }

        return new ReplaceShapes(shapes).transform(this, model);
    }

    /**
     * Removes shapes from the model while ensuring that the model is in a
     * consistent state.
     *
     * @param model Model to transform.
     * @param shapes Shapes to add or replace in the model.base.
     * @return Returns the transformed model.base.
     */
    public Model removeShapes(Model model, Collection shapes) {
        if (shapes.isEmpty()) {
            return model;
        }

        return new RemoveShapes(shapes, plugins).transform(this, model);
    }

    /**
     * Removes shapes from the model that match the given predicate.
     *
     * @param model Model to transform.
     * @param predicate Predicate that accepts a shape and returns true to
     *  remove it.
     * @return Returns the transformed model.base.
     */
    public Model removeShapesIf(Model model, Predicate predicate) {
        return filterShapes(model, FunctionalUtils.not(predicate));
    }

    /**
     *  Renames shapes using ShapeId pairs while ensuring that the
     *  transformed model is in a consistent state.
     *
     *  

This transformer ensures that when an aggregate shape is renamed, all * members are updated in the model. * * @param model Model to transform. * @param renamed Map of shapeIds * @return Returns the transformed model.base. */ public Model renameShapes( Model model, Map renamed ) { return this.renameShapes(model, renamed, () -> Model.assembler().disableValidation()); } /** * Renames shapes using ShapeId pairs while ensuring that the * transformed model is in a consistent state. * *

This transformer ensures that when an aggregate shape is renamed, all * members are updated in the model. * * @param model Model to transform. * @param renamed Map of shapeIds * @param modelAssemblerSupplier Supplier used to create {@link ModelAssembler}s in each transform. * @return Returns the transformed model. */ public Model renameShapes( Model model, Map renamed, Supplier modelAssemblerSupplier ) { return new RenameShapes(renamed, modelAssemblerSupplier).transform(this, model); } /** * Filters shapes out of the model that do not match the given predicate. * *

This filter will never filter out shapes that are part of the * prelude. Use the {@link #removeShapes} method directly if you need * to remove traits that are in the prelude. * * @param model Model to transform. * @param predicate Predicate that filters shapes. * @return Returns the transformed model. */ public Model filterShapes(Model model, Predicate predicate) { return new FilterShapes(predicate).transform(this, model); } /** * Filters traits out of the model that do not match the given predicate. * *

The predicate function accepts the shape that a trait is attached to * and the trait. If the predicate returns false, then the trait is * removed from the shape. * * @param model Model to transform. * @param predicate Predicate that accepts a (Shape, Trait) and returns * false if the trait should be removed. * @return Returns the transformed model.base. */ public Model filterTraits(Model model, BiPredicate predicate) { return new FilterTraits(predicate).transform(this, model); } /** * Filters traits out of the model that match a predicate function. * *

The predicate function accepts the shape that a trait is attached to * and the trait. If the predicate returns true, then the trait is removed * from the shape. * * @param model Model to transform. * @param predicate Predicate that accepts a (Shape, Trait) and returns * true if the trait should be removed. * @return Returns the transformed model.base. */ public Model removeTraitsIf(Model model, BiPredicate predicate) { return filterTraits(model, predicate.negate()); } /** * Filters out metadata key-value pairs from a model that do not match * a predicate. * * @param model Model to transform. * @param predicate A predicate that accepts a metadata key-value pair. * If the predicate returns true, then the metadata key-value pair is * kept. Otherwise, it is removed. * @return Returns the transformed model.base. */ public Model filterMetadata(Model model, BiPredicate predicate) { return new FilterMetadata(predicate).transform(model); } /** * Maps over all traits in the model using a mapping function that accepts * the shape the trait is applied to, a trait, and returns a trait, * possibly even a different kind of trait. * *

An exception is thrown if a trait is returned that targets a * different shape than the {@link Shape} passed into the mapper function. * * @param model Model to transform. * @param mapper Mapping function that accepts a (Shape, Trait) and returns * the mapped Trait. * @return Returns the transformed model.base. */ public Model mapTraits(Model model, BiFunction mapper) { return new MapTraits(mapper).transform(this, model); } /** * Maps over all traits in the model using multiple mapping functions. * *

Note: passing in a list of mappers is much more efficient than * invoking {@code mapTraits} multiple times because it reduces the number * of intermediate models that are needed to perform the transformation. * * @param model Model to transform. * @param mappers Mapping functions that accepts a (Shape, Trait) and * returns the mapped Trait. * @return Returns the transformed model.base. * @see #mapShapes(Model, Function) for more information. */ public Model mapTraits(Model model, List> mappers) { return mapTraits(model, mappers.stream() .reduce((a, b) -> (s, t) -> b.apply(s, a.apply(s, t))) .orElse((s, t) -> t)); } /** * Maps over all shapes in the model using a mapping function, allowing * shapes to be replaced with completely different shapes or slightly * modified shapes. * *

An exception is thrown if a mapper returns a shape with a different * shape ID or a different type. * * @param model Model to transform. * @param mapper Mapping function that accepts a shape and returns a shape * with the same ID. * @return Returns the transformed model.base. */ public Model mapShapes(Model model, Function mapper) { return new MapShapes(mapper).transform(this, model); } /** * Maps over all shapes in the model using multiple mapping functions. * *

Note: passing in a list of mappers is much more efficient than * invoking {@code mapShapes} multiple times because it reduces the * number of intermediate models that are needed to perform the * transformation. * * @param model Model to transform. * @param mappers Mapping functions that accepts a shape and returns a * shape with the same ID. * @return Returns the transformed model.base. * @see #mapShapes(Model, Function) for more information. */ public Model mapShapes(Model model, List> mappers) { return mapShapes(model, (mappers.stream().reduce(Function::compose).orElse(Function.identity()))); } /** * Removes shapes (excluding service shapes) that are not referenced by * any other shapes. * * @param model Model to transform. * @return Returns the transformed model.base. */ public Model removeUnreferencedShapes(Model model) { return removeUnreferencedShapes(model, FunctionalUtils.alwaysTrue()); } /** * Removes shapes (excluding service shapes) that are not referenced by * any other shapes. * * Shapes that are part of the prelude or that act as the shape of any * trait, regardless of if the trait is in use in the model, are never * considered unreferenced. * * @param model Model to transform. * @param keepFilter Predicate function that accepts an unreferenced * shape and returns true to remove the shape or false to keep the shape * in the model.base. * @return Returns the transformed model.base. */ public Model removeUnreferencedShapes(Model model, Predicate keepFilter) { return removeShapes(model, new UnreferencedShapes(keepFilter).compute(model)); } /** * Removes definitions for traits that are not used by any shape in the * model.base. * * Trait definitions that are part of the prelude will not be removed. * * @param model Model to transform * @return Returns the transformed model.base. */ public Model removeUnreferencedTraitDefinitions(Model model) { return removeUnreferencedTraitDefinitions(model, FunctionalUtils.alwaysTrue()); } /** * Removes trait definitions for traits that are not used by any shape * in the model. * *

Trait definitions that are part of the prelude will not be removed. * * @param model Model to transform * @param keepFilter Predicate function that accepts an unreferenced trait * shape (that has the {@link TraitDefinition} trait) and returns true to * remove the definition or false to keep the definition in the model.base. * @return Returns the transformed model.base. */ public Model removeUnreferencedTraitDefinitions(Model model, Predicate keepFilter) { return removeShapes(model, new UnreferencedTraitDefinitions(keepFilter).compute(model)); } /** * Removes all trait definitions from a model and all shapes that are * only connected to the graph either directly or transitively by a * trait definition shape. * *

This can be useful when serializing a Smithy model to a format that * does not include trait definitions and the shapes used by trait definitions * would have no meaning (e.g., OpenAPI). * * @param model Model to transform. * @return Returns the transformed model.base. */ public Model scrubTraitDefinitions(Model model) { return scrubTraitDefinitions(model, FunctionalUtils.alwaysTrue()); } /** * Removes trait definitions from a model and all shapes that are * only connected to the graph either directly or transitively by a * trait definition shape. * *

This can be useful when serializing a Smithy model to a format that * does not include trait definitions and the shapes used by trait definitions * would have no meaning (e.g., OpenAPI). * * @param model Model to transform. * @param keepFilter Predicate function that accepts an trait shape (that * has the {@link TraitDefinition} trait) and returns true to remove the * definition or false to keep the definition in the model. * @return Returns the transformed model. */ public Model scrubTraitDefinitions(Model model, Predicate keepFilter) { return new ScrubTraitDefinitions().transform(this, model, keepFilter); } /** * Gets all shapes from a model where shapes that define traits or shapes * that are only used as part of a trait definition have been removed. * * @param model Model that contains shapes. * @return Returns a model that contains matching shapes. */ public Model getModelWithoutTraitShapes(Model model) { return getModelWithoutTraitShapes(model, FunctionalUtils.alwaysTrue()); } /** * Gets all shapes from a model where shapes that define traits or shapes * that are only used as part of a trait definition have been removed. * * @param model Model that contains shapes. * @param keepFilter Predicate function that accepts a trait shape (that * has the {@link TraitDefinition} trait) and returns true to remove the * definition or false to keep the definition in the model. * @return Returns a model that contains matching shapes. */ public Model getModelWithoutTraitShapes(Model model, Predicate keepFilter) { Model.Builder builder = Model.builder(); // ScrubTraitDefinitions is used to removed traits and trait shapes. // However, the returned model can't be returned directly because // as traits are removed, uses of that trait are removed. Instead, // a model is created by getting all shape IDs from the modified // model, grabbing shapes from the original model, and building a new // Model. scrubTraitDefinitions(model, keepFilter).shapes() .map(Shape::getId) .map(model::getShape) .map(Optional::get) .forEach(builder::addShape); return builder.build(); } /** * Reorders the members of structure and union shapes using the given * {@link Comparator}. * *

Note that by default, Smithy models retain the order in which * members are defined in the model. However, in programming languages * where this isn't important, it may be desirable to order members * alphabetically or using some other kind of order. * * @param model Model that contains shapes. * @param comparator Comparator used to order members of unions and structures. * @return Returns a model that contains matching shapes. */ public Model sortMembers(Model model, Comparator comparator) { return new SortMembers(comparator).transform(this, model); } /** * Changes the type of each given shape. * *

The following transformations are permitted: * *

    *
  • Any simple type to any simple type
  • *
  • List to set
  • *
  • Set to list
  • *
  • Structure to union
  • *
  • Union to structure
  • *
* * @param model Model to transform. * @param shapeToType Map of shape IDs to the new type to use for the shape. * @return Returns the transformed model. * @throws ModelTransformException if an incompatible type transform is attempted. */ public Model changeShapeType(Model model, Map shapeToType) { return new ChangeShapeType(shapeToType).transform(this, model); } /** * Changes the type of each given shape. * *

The following transformations are permitted: * *

    *
  • Any simple type to any simple type
  • *
  • List to set
  • *
  • Set to list
  • *
  • Structure to union
  • *
  • Union to structure
  • *
* * @param model Model to transform. * @param shapeToType Map of shape IDs to the new type to use for the shape. * @param changeShapeTypeOptions An array of options to enable when changing types. * @return Returns the transformed model. * @throws ModelTransformException if an incompatible type transform is attempted. */ public Model changeShapeType( Model model, Map shapeToType, ChangeShapeTypeOption... changeShapeTypeOptions ) { boolean synthesizeNames = ChangeShapeTypeOption.SYNTHESIZE_ENUM_NAMES.hasFeature(changeShapeTypeOptions); return new ChangeShapeType(shapeToType, synthesizeNames).transform(this, model); } /** * Changes each compatible string shape with the enum trait to an enum shape. * *

A member will be created on the shape for each entry in the {@link EnumTrait}. * *

When the enum definition from the enum trait has been marked as deprecated, or * tagged as "internal", the corresponding enum shape member will be marked with the * DeprecatedTrait or InternalTrait accordingly. * * @param model Model to transform. * @param synthesizeEnumNames Whether enums without names should have names synthesized if possible. * @return Returns the transformed model. */ public Model changeStringEnumsToEnumShapes(Model model, boolean synthesizeEnumNames) { return ChangeShapeType.upgradeEnums(model, synthesizeEnumNames).transform(this, model); } /** * Changes each compatible string shape with the enum trait to an enum shape. * *

A member will be created on the shape for each entry in the {@link EnumTrait}. * *

Strings with enum traits that don't define names are not converted. * * @param model Model to transform. * @return Returns the transformed model. */ public Model changeStringEnumsToEnumShapes(Model model) { return ChangeShapeType.upgradeEnums(model, false).transform(this, model); } /** * Changes each enum shape to a string shape and each intEnum to an integer. * * @param model Model to transform. * @return Returns the transformed model. */ public Model downgradeEnums(Model model) { return ChangeShapeType.downgradeEnums(model).transform(this, model); } /** * Copies the errors defined on the given service onto each operation bound to the * service, effectively flattening service error inheritance. * * @param model Model to modify. * @param forService Service shape to use as the basis for copying errors to operations. * @return Returns the transformed model. */ public Model copyServiceErrorsToOperations(Model model, ServiceShape forService) { return new CopyServiceErrorsToOperationsTransform(forService).transform(this, model); } /** * Updates the model so that every operation has a dedicated input shape marked * with the {@code input} trait and output shape marked with the {@code output} * trait, and the targeted shapes all have a consistent shape name of * OperationName + {@code inputSuffix} / {@code outputSuffix} depending on the * context. * *

If an operation's input already targets a shape marked with the {@code input} * trait, then the existing input shape is used as input, though the shape will * be renamed if it does not use the given {@code inputSuffix}. If an operation's * output already targets a shape marked with the {@code output} trait, then the * existing output shape is used as output, though the shape will be renamed if it * does not use the given {@code outputSuffix}. * *

If the operation's input shape starts with the name of the operation and is * only used throughout the model as the input of the operation, then it is updated * to have the {@code input} trait, and the name remains unaltered. * *

If the operation's output shape starts with the name of the operation and is * only used throughout the model as the output of the operation, then it is updated * to have the {@code output} trait, and the name remains unaltered. * *

If the operation's input shape does not start with the operation's name or * is used in other places throughout the model, a copy of the targeted input * structure is created, the name of the shape becomes OperationName + {@code inputSuffix}, * and the {@code input} trait is added to the shape. The operation is then updated * to target the created shape, and the original shape is left as-is in the model. * *

If the operation's output shape does not start with the operation's name or * is used in other places throughout the model, a copy of the targeted output * structure is created, the name of the shape becomes OperationName + {@code outputSuffix}, * and the {@code output} trait is added to the shape. The operation is then updated * to target the created shape, and the original shape is left as-is in the model. * *

If a naming conflict occurs while attempting to create a new shape, then * the default naming conflict resolver will attempt to name the shape * OperationName + "Operation" + {@code inputSuffix} / {@code outputSuffix} * depending on the context. If the name is still in conflict with other * shapes in the model, then a {@link ModelTransformException} is thrown. * *

Any time a shape is renamed, the original shape ID of the shape is captured * on the shape using the synthetic {@link OriginalShapeIdTrait}. This might be * useful for protocols that need to serialize input and output shape names. * * @param model Model to update. * @param inputSuffix Suffix to append to dedicated input shapes (e.g., "Input"). * @param outputSuffix Suffix to append to dedicated input shapes (e.g., "Output"). * @return Returns the updated model. * @throws ModelTransformException if an input or output shape name conflict occurs. */ public Model createDedicatedInputAndOutput(Model model, String inputSuffix, String outputSuffix) { return new CreateDedicatedInputAndOutput(inputSuffix, outputSuffix).transform(this, model); } /** * Flattens mixins out of the model and removes them from the model. * * @param model Model to flatten. * @return Returns the flattened model. */ public Model flattenAndRemoveMixins(Model model) { return new FlattenAndRemoveMixins().transform(this, model); } /** * Add the clientOptional trait to members that are effectively nullable because * they are part of a structure marked with the input trait or they aren't required * and don't have a default trait. * * @param model Model to transform. * @param applyWhenNoDefaultValue Set to true to add clientOptional to members that target * shapes with no zero value (e.g., structure and union). * @return Returns the transformed model. */ public Model addClientOptional(Model model, boolean applyWhenNoDefaultValue) { return new AddClientOptional(applyWhenNoDefaultValue).transform(this, model); } /** * Removes Smithy IDL 2.0 features from a model that are not strictly necessary to keep for consistency with the * rest of Smithy. * *

This transformer is lossy, and converts enum shapes to string shapes with the enum trait, intEnum shapes to * integer shapes, flattens and removes mixins, removes properties from resources, and removes default traits that * have no impact on IDL 1.0 semantics (i.e., default traits on structure members set to something other than null, * or default traits on any other shape that are not the zero value of the shape of a 1.0 model). * * @param model Model to downgrade. * @return Returns the downgraded model. */ public Model downgradeToV1(Model model) { return new DowngradeToV1().transform(this, model); } /** * Remove default traits if the default conflicts with the range trait of the shape. * * @param model Model to transform. * @return Returns the transformed model. */ public Model removeInvalidDefaults(Model model) { return new RemoveInvalidDefaults().transform(this, model); } /** * Deconflicts errors that share a status code. * * @param model Model to transform. * @return Returns the transformed model. */ public Model deconflictErrorsWithSharedStatusCode(Model model, ServiceShape forService) { return new DeconflictErrorsWithSharedStatusCode(forService).transform(this, model); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy