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

software.amazon.smithy.build.SmithyBuild Maven / Gradle / Ivy

Go to download

This module is a library used to validate Smithy models, create filtered projections of a model, and generate build artifacts.

There is a newer version: 1.51.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.build;

import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.logging.Logger;
import software.amazon.smithy.build.model.SmithyBuildConfig;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.loader.ModelAssembler;
import software.amazon.smithy.model.transform.ModelTransformer;

/**
 * Runs the projections and plugins found in a {@link SmithyBuildConfig}
 * and writes the artifacts to a {@link FileManifest}.
 */
public final class SmithyBuild {
    /** The version of Smithy build. */
    public static final String VERSION = "1.0";

    private static final Logger LOGGER = Logger.getLogger(SmithyBuild.class.getName());
    private static final PathMatcher VALID_MODELS = FileSystems.getDefault().getPathMatcher("glob:*.{json,jar,smithy}");

    SmithyBuildConfig config;
    Path outputDirectory;
    Function> transformFactory;
    Function> pluginFactory;
    Function fileManifestFactory;
    Supplier modelAssemblerSupplier;
    ModelTransformer modelTransformer;
    Model model;
    ClassLoader pluginClassLoader;
    Set sources = new HashSet<>();
    Predicate projectionFilter = name -> true;
    Predicate pluginFilter = name -> true;

    public SmithyBuild() {}

    public SmithyBuild(SmithyBuildConfig config) {
        config(config);
    }

    /**
     * Creates a {@code SmithyBuild} implementation that is configured to
     * discover various Smithy service providers using the given
     * {@code ClassLoader}.
     *
     * @param classLoader ClassLoader used to discover service providers.
     * @return Returns the created {@code SmithyBuild} object.
     */
    public static SmithyBuild create(ClassLoader classLoader) {
        ModelAssembler assembler = Model.assembler(classLoader);
        return create(classLoader, assembler::copy);
    }

    /**
     * Creates a {@code SmithyBuild} implementation that is configured to
     * discover various Smithy service providers using the given
     * {@code ClassLoader}.
     *
     * @param classLoader ClassLoader used to discover service providers.
     * @param modelAssemblerSupplier Supplier used to create {@link ModelAssembler}s in each build.
     * @return Returns the created {@code SmithyBuild} object.
     */
    public static SmithyBuild create(ClassLoader classLoader, Supplier modelAssemblerSupplier) {
        return new SmithyBuild()
                .modelAssemblerSupplier(modelAssemblerSupplier)
                .modelTransformer(ModelTransformer.createWithServiceProviders(classLoader))
                .transformFactory(ProjectionTransformer.createServiceFactory(classLoader))
                .pluginFactory(SmithyBuildPlugin.createServiceFactory(classLoader))
                .pluginClassLoader(classLoader);
    }

    /**
     * Gets the default directory where smithy-build artifacts are written.
     *
     * @return Returns the build output path.
     */
    public static Path getDefaultOutputDirectory() {
        return DefaultPathHolder.DEFAULT_PATH;
    }

    /**
     * Builds the model and applies all projections.
     *
     * 

This method loads all projections, projected models, and their * results into memory so that a {@link SmithyBuildResult} can be * returned. See {@link #build(Consumer, BiConsumer)} for a streaming * approach that uses callbacks and does not load all projections into * memory at once. * *

Errors are aggregated together into a single * {@link SmithyBuildException} that contains an aggregated error * message and each encountered exception is registered to the aggregate * exception through {@link Throwable#addSuppressed(Throwable)}. * * @return Returns the result of building the model. * @throws IllegalStateException if a {@link SmithyBuildConfig} is not set. * @throws SmithyBuildException if the build fails. * @see #build(Consumer, BiConsumer) */ public SmithyBuildResult build() { SmithyBuildResult.Builder resultBuilder = SmithyBuildResult.builder(); Map errors = Collections.synchronizedMap(new TreeMap<>()); build(resultBuilder::addProjectionResult, errors::put); if (!errors.isEmpty()) { StringBuilder message = new StringBuilder(); message.append(errors.size()).append(" Smithy build projections failed."); message.append(System.lineSeparator()).append(System.lineSeparator()); for (Map.Entry e : errors.entrySet()) { message.append("(").append(e.getKey()).append("): ") .append(e.getValue()) .append(System.lineSeparator()); } SmithyBuildException buildException = new SmithyBuildException(message.toString()); errors.values().forEach(buildException::addSuppressed); throw buildException; } return resultBuilder.build(); } /** * Builds the model and applies all projections, passing each * {@link ProjectionResult} to the provided callback as they are * completed and each encountered exception to the provided * {@code exceptionCallback} as they are encountered. * *

This method differs from {@link #build()} in that it does not * require every projection and projection result to be loaded into * memory. * *

The result each projection is placed in the outputDirectory. * A {@code [projection]-build-info.json} file is created in the output * directory. A directory is created for each projection using the * projection name, and a file named model.json is place in each directory. * * @param resultCallback A thread-safe callback that receives projection * results as they complete. * @param exceptionCallback A thread-safe callback that receives the name * of each failed projection and the exception that occurred. * @throws IllegalStateException if a {@link SmithyBuildConfig} is not set. */ public void build(Consumer resultCallback, BiConsumer exceptionCallback) { new SmithyBuildImpl(this).applyAllProjections(resultCallback, exceptionCallback); } /** * Sets the required configuration object used to * build the model. * * @param config Configuration to set. * @return Returns the builder. */ public SmithyBuild config(SmithyBuildConfig config) { this.config = config; for (String source : config.getSources()) { addSource(Paths.get(source)); } return this; } // Add a source path using absolute paths to better de-conflict source files. ModelAssembler also // de-conflicts imports with absolute paths, but this ensures the same file doesn't appear twice in // the build plugin output (though it does not use realpath to de-conflict based on symlinks). // // Ignores and logs when an unsupported model file is encountered. private void addSource(Path path) { if (Files.isRegularFile(path) && !VALID_MODELS.matches(path.getFileName())) { LOGGER.warning("Omitting unsupported Smithy model file from model sources: " + path); } else { sources.add(path.toAbsolutePath()); } } /** * Sets the required configuration object used to * build the model. * * @param configPath Path to the configuration to set. * @return Returns the builder. */ public SmithyBuild config(Path configPath) { return config(SmithyBuildConfig.load(configPath)); } /** * Sets an optional model to use with the build. The provided model is * used alongside any "imports" found in the configuration object. * * @param model Model to build. * @return Returns the builder. */ public SmithyBuild model(Model model) { this.model = model; return this; } @Deprecated public SmithyBuild importBasePath(Path importBasePath) { return this; } /** * Set a directory where the build artifacts are written. * *

Calling this method will supersede any {@code outputDirectory} * setting returned by {@link SmithyBuildConfig#getOutputDirectory()}. * *

If no output directory is specified here or in the config, then * a default output directory of the current working directory resolved * with {@code ./build/smithy} is used. * * @param outputDirectory Directory where artifacts are written. * @return Returns the builder. * * @see SmithyBuildConfig#getOutputDirectory */ public SmithyBuild outputDirectory(Path outputDirectory) { this.outputDirectory = outputDirectory; return this; } /** * Set a directory where the build artifacts are written. * *

Calling this method will supersede any {@code outputDirectory} * setting returned by {@link SmithyBuildConfig#getOutputDirectory()}. * *

If no output directory is specified here or in the config, then * a default output directory of the current working directory + * {@code ./build/smithy} is used. * * @param outputDirectory Directory where artifacts are written. * @return Returns the builder. * * @see SmithyBuildConfig#getOutputDirectory */ public SmithyBuild outputDirectory(String outputDirectory) { return outputDirectory(Paths.get(outputDirectory)); } /** * Sets a factory function that's used to create {@link FileManifest} * objects when writing {@link SmithyBuildPlugin} artifacts. * *

A default implementation of {@link FileManifest#create} will be * used if a custom factory is not provided. * * @param fileManifestFactory Factory that accepts a base path and * returns a {@link FileManifest}. * @return Returns the builder. */ public SmithyBuild fileManifestFactory(Function fileManifestFactory) { this.fileManifestFactory = fileManifestFactory; return this; } /** * Called to create {@link ModelAssembler} to load the original * model and to load each projected model. * *

If not provided, the runner will use a default ModelAssembler * implementation that discovers traits, validators, and other * service providers using the class path and module path of * {@code software.smithy.model}. * *

Warning: this supplier can be invoked multiple times to build * a single projection, cache things like service provider factories * when necessary. The same instance of a ModelAssembler MUST NOT * be returned from successive calls to the supplier because assemblers * are created and mutated in different threads. * * @param modelAssemblerSupplier ModelValidator supplier to utilize. * @return Returns the builder. */ public SmithyBuild modelAssemblerSupplier(Supplier modelAssemblerSupplier) { this.modelAssemblerSupplier = modelAssemblerSupplier; return this; } /** * Sets a custom {@link ModelTransformer} to use when building * projections. * *

The runner will use a default ModelTransformer if one is not * provided. * * @param modelTransformer Transformer to set. * @return Returns the builder. */ public SmithyBuild modelTransformer(ModelTransformer modelTransformer) { this.modelTransformer = modelTransformer; return this; } /** * Sets a factory function used to create transforms by name. * *

A default implementation that utilizes Java SPI to discover * implementations of {@link ProjectionTransformer} will be * used if a custom factory is not provided. * * @param transformFactory Factory that accepts a transform name and * returns the optionally found transform. * @return Returns the builder. * * @see ProjectionTransformer#createServiceFactory */ public SmithyBuild transformFactory(Function> transformFactory) { this.transformFactory = transformFactory; return this; } /** * Sets a factory function used to create plugins by name. * *

A default implementation that utilizes Java SPI to discover * implementations of {@link SmithyBuildPlugin} will be used if a * custom factory is not provided. * * @param pluginFactory Plugin factory that accepts a plugin name and * returns the optionally found plugin. * @return Returns the builder. * * @see SmithyBuildPlugin#createServiceFactory */ public SmithyBuild pluginFactory(Function> pluginFactory) { this.pluginFactory = pluginFactory; return this; } /** * Sets a ClassLoader that should be used by SmithyBuild plugins when * discovering services. * * @param pluginClassLoader ClassLoader plugins discover services with. * @return Returns the builder. */ public SmithyBuild pluginClassLoader(ClassLoader pluginClassLoader) { this.pluginClassLoader = pluginClassLoader; return this; } /** * Registers the given paths as sources of the model being built. * *

There are typically two kinds of models that are added to a build: * source models and discovered models. Discovered models are someone * else's models. Source models are the models owned by the package * being built. * *

Source models are copied into the automatically executed "manifest" * plugin. If no transformations were applied to the sources, then the * source models are copied literally into the manifest directory output. * Otherwise, a modified version of the source models are copied. * *

When a directory is provided, all of the files in the directory are * treated as sources, and they are relativized to remove the directory. * When a file is provided, the directory that contains that file is used * as a source. All of the relativized files resolved in sources must be * unique across the entire set of files. The sources directories are * essentially flattened into a single directory. * *

Unsupported model files are ignored and not treated as sources. * This can happen when adding model files from a directory that contains * a mix of model files and non-model files. Filtering models here prevents * unsupported files from appearing in places like JAR manifest files where * they are not allowed. * * @param pathToSources Path to source directories to mark. * @return Returns the builder. */ public SmithyBuild registerSources(Path... pathToSources) { for (Path path : pathToSources) { addSource(path); } return this; } /** * Sets a predicate that accepts the name of a projection and returns * true if the projection should be built. * * @param projectionFilter Predicate that accepts a projection name. * @return Returns the builder. */ public SmithyBuild projectionFilter(Predicate projectionFilter) { this.projectionFilter = Objects.requireNonNull(projectionFilter); return this; } /** * Sets a predicate that accepts the name of a plugin and returns * true if the plugin should be built. * * @param pluginFilter Predicate that accepts a projection name. * @return Returns the builder. */ public SmithyBuild pluginFilter(Predicate pluginFilter) { this.pluginFilter = Objects.requireNonNull(pluginFilter); return this; } // Lazy initialization holder class idiom. private static final class DefaultPathHolder { private static final Path DEFAULT_PATH = resolveDefaultPath(); private static Path resolveDefaultPath() { return Paths.get(".").toAbsolutePath().normalize().resolve("build").resolve("smithy"); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy