Mercurial > hg > thermostat-ng
view common/src/main/java/com/redhat/thermostat/common/swaggercombine/SwaggerCombine.java @ 26:33cf0b946e3d
Add SwaggerCombine utility
This patch adds a new utility class SwaggerCombine, that, given a list of Swagger files, will create one massive Swagger file.
Any differing elements that do not have the key "description" are flagged - descriptions are allow to differ.
The basic usage is:
SwaggerCombine [--yaml|--json] [--pretty] [--quiet] infile1 [infile2]...
--yaml or --json write the combined file to stdout, --pretty indents JSON, and --quiet omitsprocessing messages.
The default is to combine all files and only output error and processing messages.
Reviewed-by: sgehwolf
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-September/025103.html
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-October/025262.html
author | stooke@redhat.com |
---|---|
date | Wed, 11 Oct 2017 10:35:59 -0400 |
parents | |
children | 00540d33ce40 |
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.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 static final String SWAGGER_TEMPLATE = "/swagger-template.json"; public SwaggerCombine() { } public JsonObject run(final SwaggerCombineContext ctx, Writer out) throws IOException { JsonObject root = processAPIs(ctx); writeObject(root, out, ctx.getFmt() == SwaggerCombineContext.OutputFormat.YAML, ctx.isPretty()); return root; } 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); } } } private JsonObject processAPIs(final SwaggerCombineContext ctx) throws IOException { return processAPIs(ctx.getAPIList(), ctx.isQuiet(), ctx.isUseTemplate(), ctx.isLint()); } JsonObject processAPIs(final List<JsonObject> apiList, boolean quiet, boolean useTemplate, boolean doLint) throws IOException { JsonObject root = null; if (useTemplate) { final File templateFile = new File(getClass().getResource(SWAGGER_TEMPLATE).getFile()); root = processAPI(null, readSwaggerFile(templateFile), doLint); } for (JsonObject apiDef : apiList) { // read in the Swagger YAML if (!quiet) { info("processing " + apiDef.get("")); } root = processAPI(root, apiDef, 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 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) { 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()); } } 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) { System.err.println("error: " + msg); } }