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

com.nikolavp.approval.Approval Maven / Gradle / Ivy

Go to download

Approval is a Java library which will make you look at your testing from a whole different angle

There is a newer version: 0.3
Show newest version
package com.nikolavp.approval;

/*
 * #%L
 * approval
 * %%
 * Copyright (C) 2014 Nikolavp
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License 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.
 * #L%
 */

import com.nikolavp.approval.converters.Converter;
import com.nikolavp.approval.converters.Converters;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.logging.Logger;

/**
 * The main entry point class for each approval process. This is the main service class that is doing the hard work - it calls other classes for custom logic based on the object that is approved.
 * Created by nikolavp on 1/29/14.
 *
 * @param  the type of the object that will be approved by this {@link Approval}
 */
public class Approval {
    private static final Logger LOG  = Logger.getLogger(Approval.class.getName());
    private static final String FOR_APPROVAL_EXTENSION = ".forapproval";
    private final Reporter reporter;
    private final FileSystemUtils fileSystemReadWriter;
    private final Converter converter;
    private PathMapper pathMapper;

    /**
     * Create a new object that will be able to approve "things" for you.
     * @param reporter  a reporter that will be notified as needed for approval events
     * @param converter a converter that will be responsible for converting the type for approval to raw form
     * @param pathMapper the path mapper that will be used
     */
    Approval(Reporter reporter, Converter converter, @Nullable PathMapper pathMapper) {
        this(reporter, converter, pathMapper, new DefaultFileSystemUtils());
    }


    /**
     * This ctor is for testing only.
     */
    Approval(Reporter reporter, Converter converter, @Nullable PathMapper pathMapper, FileSystemUtils fileSystemReadWriter) {
        this.fileSystemReadWriter = fileSystemReadWriter;
        this.converter = converter;
        this.reporter = reporter;
        this.pathMapper = pathMapper;
    }

    /**
     * Create a new approval builder that will be able to approve objects from the specified class type.
     *
     * @param clazz the class object for the things you will be approving
     * @param    the type of the objects you will be approving
     * @return an approval builder that will be able to construct an {@link Approval} for your objects
     */
    @Nonnull
    public static  ApprovalBuilder of(Class clazz) {
        return new ApprovalBuilder(clazz);
    }

    /**
     * Get the path for approval from the original file path.
     *
     * @param filePath the original path to value
     * @return the path for approval
     */
    @Nonnull
    public static Path getApprovalPath(Path filePath) {
        Pre.notNull(filePath, "filePath");

        return FileSystems.getDefault().getPath(filePath.toString() + FOR_APPROVAL_EXTENSION);
    }

    @SuppressWarnings("unchecked")
    private static  Converter getConverterForPrimitive(Class clazz) {
        if (clazz.equals(Byte.class) || clazz.equals(byte.class)) {
            return (Converter) Converters.BYTE;
        } else if (clazz.equals(Integer.class) || clazz.equals(int.class)) {
            return (Converter) Converters.INTEGER;
        } else if (clazz.equals(String.class)) {
            return (Converter) Converters.STRING;
        } else if (clazz.equals(Short.class) || clazz.equals(short.class)) {
            return (Converter) Converters.SHORT;
        } else if (clazz.equals(Long.class) || clazz.equals(long.class)) {
            return (Converter) Converters.LONG;
        } else if (clazz.equals(Boolean.class) || clazz.equals(boolean.class)) {
            return (Converter) Converters.BOOLEAN;
        } else if (clazz.equals(Float.class) || clazz.equals(float.class)) {
            return (Converter) Converters.FLOAT;
        } else if (clazz.equals(Double.class) || clazz.equals(double.class)) {
            return (Converter) Converters.DOUBLE;
        } else if (clazz.equals(Character.class) || clazz.equals(char.class)) {
            return (Converter) Converters.CHAR;
        } else if (clazz.equals(byte[].class)) {
            return (Converter) Converters.BYTE_ARRAY;
        } else if (clazz.equals(int[].class)) {
            return (Converter) Converters.INTEGER_ARRAY;
        } else if (clazz.equals(short[].class)) {
            return (Converter) Converters.SHORT_ARRAY;
        } else if (clazz.equals(long[].class)) {
            return (Converter) Converters.LONG_ARRAY;
        } else if (clazz.equals(float[].class)) {
            return (Converter) Converters.FLOAT_ARRAY;
        } else if (clazz.equals(double[].class)) {
            return (Converter) Converters.DOUBLE_ARRAY;
        } else if (clazz.equals(boolean[].class)) {
            return (Converter) Converters.BOOLEAN_ARRAY;
        } else if (clazz.equals(char[].class)) {
            return (Converter) Converters.CHAR_ARRAY;
        } else if (clazz.equals(String[].class)) {
            return (Converter) Converters.STRING_ARRAY;
        }
        throw new IllegalArgumentException(clazz + " is not a primitive type class!");
    }

    /* Expose this to the tests */
    @Nonnull
    Converter getConverter() {
        return converter;
    }

    /* Expose this to the tests */
    @Nonnull
    Reporter getReporter() {
        return reporter;
    }

    /**
     * Verify the value that was passed in.
     *
     * @param value    the value object to be approved
     * @param filePath the path where the value will be kept for further approval
     */
    public void verify(@Nullable T value, Path filePath) {
        Pre.notNull(filePath, "filePath");

        File file = mapFilePath(value, filePath);

        File parentPathDirectory = file.getParentFile();
        if (parentPathDirectory != null && !parentPathDirectory.exists()) {
            try {
                fileSystemReadWriter.createDirectories(parentPathDirectory);
            } catch (IOException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
        }
        Path approvalPath = getApprovalPath(file.toPath());
        byte[] rawValue = converter.getRawForm(value);
        if (!file.exists()) {
            LOG.info(file + " didn't exist. You will be asked for approval");
            handleFirstTimeApproval(file.toPath(), file, approvalPath, rawValue);
            return;
        }
        try {
            byte[] fileContent = fileSystemReadWriter.readFully(file.toPath());
            if (!Arrays.equals(fileContent, rawValue)) {
                try {
                    LOG.info("Approval in " + file + " is not the same as the last value. You will be asked for approval of the new value.");
                    fileSystemReadWriter.write(approvalPath, rawValue);
                } catch (IOException e) {
                    throw new AssertionError("Couldn't write the new approval file " + file, e);
                }
                reporter.notTheSame(fileContent, file, rawValue, approvalPath.toFile());
            }
        } catch (IOException e) {
            throw new AssertionError("Couldn't read the previous content in file " + file, e);
        }

        //value approved
    }

    private void handleFirstTimeApproval(Path filePath, File file, Path approvalPath, byte[] rawValue) {
        try {
            fileSystemReadWriter.write(approvalPath, rawValue);
        } catch (IOException e) {
            throw new AssertionError("Couldn't write file for approval " + approvalPath, e);
        }
        if (reporter.approveNew(rawValue, approvalPath.toFile(), file)) {
            try {
                fileSystemReadWriter.move(approvalPath, filePath);
            } catch (IOException e) {
                String errorMessage = String.format("Couldn't move file for approval[%s] to the destination [%s]", approvalPath.toAbsolutePath(), filePath.toAbsolutePath());
                throw new AssertionError(errorMessage);
            }
        } else {
            throw new AssertionError(String.format("File %s was not approved", approvalPath.toString()));
        }
    }

    private File mapFilePath(@Nullable T value, Path filePath) {
        File file;
        if (pathMapper != null) {
            file = pathMapper.getPath(value, filePath).toFile();
        } else {
            file = filePath.toFile();
        }
        return file;
    }

    /**
     * A builder class for approvals. This is used to conveniently build new approvals for a specific type with custom reporters, converters, etc.
     *
     * @param  the type that will be approved by the the resulting approval object
     */
    public static final class ApprovalBuilder {

        private final Class clazz;
        private Converter converter;
        private Reporter reporter;
        private PathMapper pathMapper;

        private ApprovalBuilder(Class clazz) {
            this.clazz = clazz;
        }

        /**
         * Set the converter that will be used when building new approvals with this builder.
         *
         * @param converterToBeUsed the converter that will be used from the approval that will be built
         * @return the same builder for chaining
         * @see Converter
         */
        @Nonnull
        public ApprovalBuilder withConveter(Converter converterToBeUsed) {
            Pre.notNull(converterToBeUsed, "converter");

            this.converter = converterToBeUsed;
            return this;
        }

        /**
         * Set a path mapper that will be used when building the path for approval results.
         * @param pathMapperToBeUsed the path mapper
         * @return the same builder for chaining
         */
        @Nonnull
        public ApprovalBuilder withPathMapper(PathMapper pathMapperToBeUsed) {
            Pre.notNull(pathMapperToBeUsed, "pathMapper");

            this.pathMapper = pathMapperToBeUsed;
            return this;
        }

        /**
         * Creates a new approval with configuration/options(reporters, converters, etc) that were set for this builder.
         *
         * @return a new approval for the specified type with custom configuration if any
         */
        @Nonnull
        public Approval build() {
            if (converter == null) {
                try {
                    converter = getConverterForPrimitive(clazz);
                } catch (IllegalArgumentException ex) {
                    throw new IllegalStateException(String.format("You didn't provide a converter for %s and it is not a primitive type!", clazz));
                }
            }
            if (reporter == null) {
                throw new IllegalStateException("You didn't provide a reporter!");
            }
            return new Approval(reporter, converter, pathMapper);
        }

        /**
         * Set the reporter that will be used when building new approvals with this builder.
         *
         * @param reporterToBeUsed the reporter that will be used from the approval that will be built
         * @return the same builder for chaninig
         * @see Reporter
         */
        public ApprovalBuilder withReporter(Reporter reporterToBeUsed) {
            Pre.notNull(reporterToBeUsed, "reporter");

            this.reporter = reporterToBeUsed;
            return this;
        }

    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy