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

cj.restspecs.core.RestSpecValidator Maven / Gradle / Ivy

Go to download

A test-friendly mechanism for expressing RESTful http contracts. Core software module.

The newest version!
/**
 * Copyright (C) Commission Junction Inc.
 *
 * This file is part of rest-specs.
 *
 * rest-specs is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * rest-specs is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with rest-specs; see the file COPYING.  If not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 *
 * Linking this library statically or dynamically with other modules is
 * making a combined work based on this library.  Thus, the terms and
 * conditions of the GNU General Public License cover the whole
 * combination.
 *
 * As a special exception, the copyright holders of this library give you
 * permission to link this library with independent modules to produce an
 * executable, regardless of the license terms of these independent
 * modules, and to copy and distribute the resulting executable under
 * terms of your choice, provided that you also meet, for each linked
 * independent module, the terms and conditions of the license of that
 * module.  An independent module is a module which is not derived from
 * or based on this library.  If you modify this library, you may extend
 * this exception to your version of the library, but you are not
 * obligated to do so.  If you do not wish to do so, delete this
 * exception statement from your version.
 */
package cj.restspecs.core;

import java.io.File;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import cj.restspecs.core.io.ClasspathLoader;
import cj.restspecs.core.io.Loader;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public class RestSpecValidator {
    public static class FileScanningResult {
        public final List allFiles;
        public final List specDotJsFiles;

        public FileScanningResult(List allFiles, List specDotJsFiles) {
            this.allFiles = allFiles;
            this.specDotJsFiles = specDotJsFiles;
        }
    }

    public static FileScanningResult scan(File resourcesDir) {
        List allFiles = flatListFiles(new Path(), resourcesDir, new ArrayList());
        List specDotJsFiles = minusNonSpecFiles(allFiles);
        return new FileScanningResult(allFiles, specDotJsFiles);
    }

    private final File resourcesDir;
    private final Loader loader;
    private final PrintStream console;

    public static void main(String[] args) {
        new RestSpecValidator(new File(args[0])).validate();
    }

    public RestSpecValidator(File resourcesDir) {
        this(resourcesDir, new ClasspathLoader(), System.out);
    }

    public RestSpecValidator(File resourcesDir, Loader loader, PrintStream console) {
        this.resourcesDir = resourcesDir;
        this.loader = loader;
        this.console = console;
    }

    public void validate() {
        validate(Collections.emptyList());
    }

    public void validate(List ignores) {
        console.println("Scanning files under " + resourcesDir.getAbsolutePath());
        FileScanningResult scan = scan(resourcesDir);
        console.println("Found " + scan.specDotJsFiles.size() + " specs");

        if (scan.specDotJsFiles.isEmpty()) {
            throw new RuntimeException("Something is wrong ... I was expecting to find .spec.json files under " + resourcesDir.getAbsolutePath() + " but found nothing.");
        }

        Set fileNames = new TreeSet();
        for (Path specPath : scan.specDotJsFiles) {
            final String baseMessage = "ERROR VALIDATING " + new File(resourcesDir, specPath.toString());
            try {
                RestSpec spec = new RestSpec("/" + specPath.toString(), loader);

                if (spec.name() == null) {
                    throw new RuntimeException(baseMessage + ": it is missing a \"name\"");
                }

                if (spec.path() == null) {
                    throw new RuntimeException(baseMessage + ": it is missing a \"url\"");
                }

                if (spec.response() == null) {
                    throw new RuntimeException(baseMessage + ": it is missing a \"response\"");
                }

                if (spec.response().representation() != null && spec.response().header().fieldsNamed("Content-Type").isEmpty()) {
                    throw new RuntimeException(baseMessage + ": it is missing a \"Content-Type\" header");
                }

                if (fileNames.contains(spec.name())) {
                    throw new RuntimeException("There is more than one spec named \"" + spec.name() + "\"");
                } else {
                    fileNames.add(spec.name());
                }
            } catch (Exception e) {
                e.printStackTrace(console);
                throw new RuntimeException(baseMessage + ": " + e.getMessage(), e);
            }
        }

        detectOrphansAndMissingReferences(resourcesDir, scan.allFiles, scan.specDotJsFiles, ignores);
    }

    private static List minusNonSpecFiles(List allFiles) {
        List specDotJsFiles = new ArrayList();

        for (Path next : allFiles) {
            if (next.toString().toLowerCase().endsWith(".spec.json")) {
                specDotJsFiles.add(next);
            }
        }

        return specDotJsFiles;
    }


    interface Fn {
        B run(A input);
    }

    private static  List collect(List input, Fn fn) {
        List output = new ArrayList(input.size());

        for (A next : input) {
            output.add(fn.run(next));
        }

        return output;
    }

    private void detectOrphansAndMissingReferences(File resourcesDir, List files, List specDotJsFiles, List ignoreStrings) {
        List ignores = collect(ignoreStrings, new Fn() {
            public Path run(String input) {
                return new Path(input);
            }
        });
        List referencedFiles = new ArrayList();

        for (Path next : specDotJsFiles) {
            try {
                ObjectMapper mapper = new ObjectMapper();
                JsonNode root = mapper.readValue(loader.load("/" + next), JsonNode.class);

                JsonNode requestRef = root.path("request").path("representation-ref");
                if (!requestRef.isMissingNode()) {
                    Path file = validateRepresentationReference(next, requestRef);
                    referencedFiles.add(file);
                }

                JsonNode responseRef = root.path("response").path("representation-ref");
                if (!responseRef.isMissingNode()) {
                    Path file = validateRepresentationReference(next, responseRef);
                    referencedFiles.add(file);
                }
            } catch (Throwable t) {
                throw new RuntimeException("There was an error parsing " + next + " :" + t.getMessage(), t);
            }
        }

        TreeSet filesToVet = new TreeSet(files);
        filesToVet.removeAll(specDotJsFiles);
        for (Path a : referencedFiles) {
            filesToVet.remove(a);
        }

        TreeSet filesActuallyIgnored = new TreeSet();
        for (Path next : ignores) {
            if (filesToVet.contains(next)) {
                filesActuallyIgnored.add(next);
            }
        }

        if (!filesActuallyIgnored.isEmpty()) {
            System.out.println("[WARNING] The following" + filesActuallyIgnored.size() + " files are not referenced by any spec, but I've been configured to ignore them anyway.  If these are intended to be examples of request/response representations, you can fix this by referencing them with a 'representation-ref' in a .spec.json.");
            for (Path next : filesActuallyIgnored) {
                System.out.println("[WARNING]     " + next);
            }
        }
        filesToVet.removeAll(filesActuallyIgnored);

        if (!filesToVet.isEmpty()) {
            StringBuilder text = new StringBuilder("VALIDATION ERROR: The following " + filesToVet.size() + " file(s) are not expected:");
            for (Path next : filesToVet) {
                text.append("\n    " + new File(resourcesDir, next.toString()).getAbsolutePath() + "");
            }

            throw new RuntimeException(text.toString());
        }
    }

    private Path validateRepresentationReference(Path next, JsonNode refNode) {
        File file = new File(resourcesDir.getAbsolutePath() + File.separatorChar + refNode.asText());

        if (!file.exists()) {
            throw new RuntimeException("Spec references nonexistent file: " + file.getAbsolutePath());
        }

        return new Path(refNode.asText());
    }

    private static List flatListFiles(Path base, File path, List files) {
        if (path.isDirectory()) {
            for (File child : path.listFiles()) {
                flatListFiles(base.childNamed(child.getName()), child, files);
            }
        } else if (path.isFile()) {
            files.add(base);
        } else {
            // just ignore it
        }

        return files;
    }

    public static class Path implements Comparable {
        final List segments;

        public Path() {
            segments = Collections.emptyList();
        }

        public Path(String path) {
            this(Arrays.asList(path.split("/")));
        }

        public Path(List segments) {
            super();
            this.segments = segments;
        }

        public Path parent() {
            return segments.isEmpty() ? null : new Path(segments.subList(0, segments.size() - 1));
        }

        public int compareTo(Path o) {
            return o.toString().compareTo(this.toString());
        }

        public Path childNamed(String childName) {
            List path = new ArrayList();
            path.addAll(segments);
            path.add(childName);
            return new Path(path);
        }

        @Override
        public String toString() {
            StringBuilder txt = new StringBuilder();
            for (String next : segments) {
                if (txt.length() > 0) {
                    txt.append("/");
                }
                txt.append(next);
            }

            return txt.toString();
        }
    }
}