Mercurial > hg > thermostat-ng
view common/src/main/java/com/redhat/thermostat/common/swaggercombine/SwaggerCombine.java @ 35:76d1ce01cc7a default tip master
Fail build on YAML lint error
This patch will cause SwaggerCombine to throw an exception if "--throw"
is specified on the command line and there is a lint error in the YAML
files.
Currently there are no such messages.
Reviewed-by: neugens
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-October/025574.html
author | Simon Tooke <stooke@redhat.com> |
---|---|
date | Tue, 31 Oct 2017 09:49:01 -0400 |
parents | 00540d33ce40 |
children |
line wrap: on
line source
/* * Copyright 2012-2017 Red Hat, Inc. * * This file is part of Thermostat. * * Thermostat 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. * * Thermostat 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 Thermostat; see the file COPYING. If not see * <http://www.gnu.org/licenses/>. * * Linking this code with other modules is making a combined work * based on this code. Thus, the terms and conditions of the GNU * General Public License cover the whole combination. * * As a special exception, the copyright holders of this code give * you permission to link this code 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 code. If you modify * this code, 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 com.redhat.thermostat.common.swaggercombine; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringReader; import java.io.Writer; import java.util.List; import java.util.Map; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.redhat.thermostat.common.json.JsonUtil; import com.redhat.thermostat.common.yaml.JsonToYaml; import com.redhat.thermostat.common.yaml.RefResolver; import com.redhat.thermostat.common.yaml.YamlToJson; /** * Utility to combine multiple Swagger API definitions into one */ public class SwaggerCombine { private Gson gson = null; private Gson prettyGson = null; private int lintCount = 0; private static final String SWAGGER_TEMPLATE = "/swagger-template.json"; public SwaggerCombine() { } /** * perform combine, writing output to a Writer * @param ctx input context object * @param out output writer, to which YAML or JSON is writen depending on the context * @return object representing the combined context * @throws IOException on i/o or syntax error */ public JsonObject run(final SwaggerCombineContext ctx, Writer out) throws IOException { JsonObject root = run(ctx); writeObject(root, out, ctx.getFmt() == SwaggerCombineContext.OutputFormat.YAML, ctx.isPretty()); return root; } /** * perform combine * @param ctx input context object * @return object representing the combined context * @throws IOException on i/o or syntax error */ public JsonObject run(final SwaggerCombineContext ctx) throws IOException { return processAPIs(ctx.getAPIList(), ctx.isQuiet(), ctx.isUseTemplate(), ctx.isLint()); } /** * return number of lint messages * @return number of lint messages */ public int getLintCount() { return lintCount; } private void writeObject(JsonObject root, Writer out, boolean toYaml, boolean pretty) throws IOException { if (root != null) { if (toYaml) { new JsonToYaml().jsonToYaml(root, out); } else { getGsonInstance(pretty).toJson(root, out); } } } JsonObject processAPIs(final List<SwaggerCombineContext.MicroAPI> apiList, boolean quiet, boolean useTemplate, boolean doLint) throws IOException { JsonObject root = null; if (useTemplate) { root = processAPI(null, readSwaggerStream(getClass().getResourceAsStream(SWAGGER_TEMPLATE), false), doLint); } for (SwaggerCombineContext.MicroAPI apiArg : apiList) { final JsonObject json; switch (apiArg.getFormat()) { case YAML_STRING: final YamlToJson y2j = new YamlToJson(); json = y2j.yamlToJsonObject(apiArg.getData()); break; case JSON_STRING: final JsonParser parser = new JsonParser(); json = parser.parse(new BufferedReader(new StringReader(apiArg.getData()))).getAsJsonObject(); break; case FILE: // read in the Swagger YAML if (!quiet) { info("processing " + apiArg.getInFile()); } json = readSwaggerFile(apiArg.getInFile()); break; default: // just to get rid of compiler warning json = null; } root = processAPI(root, json, doLint); } return root; } private JsonObject processAPI(JsonObject root, final JsonObject apiDef, boolean doLint) throws IOException { // see if we should run some tests if (doLint && apiDef != null) { lint(apiDef); } // if it's the first, take it directly if (root == null) { root = apiDef; } else { combineSwagger(root, apiDef); } return root; } private void lint(JsonObject in) { // mke a copy because we want to test reference resolving (combineSwagger doesn't want references resolved) final JsonObject copy = JsonUtil.deepCopy(in).getAsJsonObject(); try { RefResolver.resolveRefs(in, copy); } catch (IOException e) { // exceptiuon here means there were issues lintCount++; System.err.println("lint: " + e.getMessage()); } // TODO: test for mandatory elements // TODO: test for unused definitions } private void combineSwagger(JsonObject root, JsonObject in) { // assumes no references are resolved /* * first, compare header for compatiblity * * - the info section is probably different (but the license should be the same) * - comsumes and produces become unions * - basepath should be longest common path but instead we make it '/' and prepend it to every path in the API spec */ // only once, we fix up the root swagger files basePath if (! "/".equals(root.get("basePath").getAsString())) { prependBasepath(root); } prependBasepath(in); addGently(root, "consumes", in.getAsJsonArray("consumes")); addGently(root, "produces", in.getAsJsonArray("produces")); addGently(root, "paths", in.getAsJsonObject("paths")); addGently(root, "definitions", in.getAsJsonObject("definitions")); addGently(root, "parameters", in.getAsJsonObject("parameters")); } private static class Pair { final JsonElement e1; final JsonElement e2; Pair(JsonElement e1, JsonElement e2) { this.e1 = e1; this.e2 = e2; } } private Pair compareGently(JsonElement e1, JsonElement e2) { if (!e1.getClass().equals(e2.getClass())) return new Pair(e1, e2); if (e1.isJsonObject()) { // compare all elements but ignore "description" JsonObject o1 = e1.getAsJsonObject(); JsonObject o2 = e2.getAsJsonObject(); // look for keys in o2 but not e1 (flag 'em) or if they both have them ,compare recursively for (final Map.Entry<String, JsonElement> entry2 : o2.entrySet()) { if (entry2.getKey().equals("description")) continue; if (o1.has(entry2.getKey())) { Pair p = compareGently(o1.get(entry2.getKey()), entry2.getValue()); if (p != null) { return p; } } else { return new Pair(e1, e2); } } // now look for keys in e1 but not e2 for (final Map.Entry<String, JsonElement> entry3 : o1.entrySet()) { if (entry3.getKey().equals("description")) continue; if (!o2.has(entry3.getKey())) { return new Pair(e1, e2); } } } else { if (!e1.equals(e2)) { return new Pair(e1, e2); } } return null; } private void addGently(JsonObject destroot, String name, JsonObject src) { if (src == null) { return; // nothing to check } if (!destroot.has(name)) { destroot.add(name, new JsonObject()); } JsonObject dest = destroot.getAsJsonObject(name); for (final Map.Entry<String, JsonElement> entry : src.entrySet()) { if (entry.getKey() == null) { lintCount++; error("key is null"); } if (dest.has(entry.getKey())) { JsonElement haveAlready = dest.get(entry.getKey()); Pair pair = compareGently(haveAlready, entry.getValue()); if (pair != null) { error("key " + entry.getKey() + " differs: " + pair.e1 + " and " + pair.e2); } } else { dest.add(entry.getKey(), entry.getValue()); } } } private void addGently(JsonObject destroot, String name, JsonArray src) { if (src == null) { return; // nothing to check } if (!destroot.has(name)) { destroot.add(name, new JsonArray()); } JsonArray dest = destroot.getAsJsonArray(name); for (final JsonElement entry : src) { if (!dest.contains(entry)) { dest.add(entry); } } } private void prependBasepath(JsonObject root) { final String basePath = root.get("basePath").getAsString(); prependBasepath(root, basePath); root.addProperty("basePath", "/"); } private void prependBasepath(JsonObject root, String basePath) { JsonObject originalPaths = root.getAsJsonObject("paths"); JsonObject paths = JsonUtil.deepCopy(originalPaths).getAsJsonObject(); for (final Map.Entry<String, JsonElement> entry : paths.entrySet()) { originalPaths.remove(entry.getKey()); originalPaths.add(basePath + entry.getKey(), entry.getValue()); } } /** * read a JSON or YAML stream into a JsonObject * @param in input stream * @param isYaml true if YAML format, false if JSON format * @return a JsonObject representing the input stream contents * @throws IOException if there was an error reading, or an error in format */ public static JsonObject readSwaggerStream(InputStream in, boolean isYaml) throws IOException { final JsonObject json; if (isYaml) { final YamlToJson y2j = new YamlToJson(); json = y2j.yamlToJsonObject(new BufferedReader(new InputStreamReader(in))); } else { final JsonParser parser = new JsonParser(); json = parser.parse(new BufferedReader(new InputStreamReader(in))).getAsJsonObject(); } return json; } /** * read a JSON or YAML file into a JsonObject * @param fin input file (must end in ".yaml" or ".json") * @return a JsonObject representing the filecontents * @throws IOException if there was an error reading, or an error in format */ public static JsonObject readSwaggerFile(File fin) throws IOException { final JsonObject json; if (fin.getName().endsWith(".yaml")) { final YamlToJson y2j = new YamlToJson(); json = y2j.yamlToJsonObject(fin); } else if (fin.getName().endsWith(".json")) { final JsonParser parser = new JsonParser(); json = parser.parse(new BufferedReader(new FileReader(fin))).getAsJsonObject(); } else { // only to clean up warning message - we never get here as we filteres out all invalid filenames earlier throw new IOException("file '" + fin + "' has neither .yaml nor .json extension"); } return json; } private Gson getGsonInstance(boolean isPretty) { if (isPretty) { if (prettyGson == null) { final GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.setPrettyPrinting(); prettyGson = gsonBuilder.create(); } return prettyGson; } else { if (gson == null) { gson = new Gson(); } return gson; } } private void info(String msg) { System.err.println(msg); } protected void error(String msg) { lintCount++; System.err.println("error: " + msg); } }