/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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. */ package org.jooby.swagger; import static java.util.Objects.requireNonNull; import java.io.IOException; import java.io.InputStream; import java.util.Iterator; import java.util.Properties; import java.util.function.Function; import java.util.function.Predicate; import java.util.regex.Pattern; import org.jooby.Jooby; import org.jooby.MediaType; import org.jooby.Results; import org.jooby.internal.swagger.SwaggerBuilder; import org.jooby.internal.swagger.SwaggerHandler; import org.jooby.internal.swagger.SwaggerModule; import org.jooby.internal.swagger.SwaggerYml; import org.jooby.spec.RouteSpec; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.google.common.base.Splitter; import io.swagger.models.Swagger; import io.swagger.util.Json; import io.swagger.util.Yaml; /** * <h1>swagger module</h1> * * <p> * Generates a <a href="http://swagger.io">Swagger spec</a> from an application. * </p> * * <p> * <strong>NOTE:</strong> This modules depends on {@link RouteSpec}, please read the * {@link RouteSpec} to learn how to use this tool. * </p> * * <h2>exposes</h2> * <ul> * <li>A <code>/swagger</code> route that will render a Swagger UI</li> * <li>A <code>/swagger.json</code> route that will render a Swagger Spec in json format</li> * <li>A <code>/swagger.yml</code> route that will render a Swagger Spec in yaml format</li> * </ul> * * <h2>usage</h2> * * <p> * via Script API: * </p> * * <pre> * { * /{@literal *}{@literal *} * {@literal *}Everything about your pets * {@literal *}/ * use("/api/pets") * /{@literal *}{@literal *} * {@literal *} Get a pet by ID. * {@literal *} @param id Pet ID * {@literal *}/ * .get("/:id", req {@literal ->} { * int id = req.param("id").intValue(); * DB db = req.require(DB.class); * Pet pet = db.find(Pet.class, id); * return pet; * }) * ...; * } * </pre> * * <p> * via MVC API: * </p> * <pre> * @Path("/api/pets") * public class Pets { * * @Path("/:id") * @GET * public Pet get(String id) {...} * * @POST * public Pet post(Pet pet) {...} * } * * { * SwaggerUI.install(this); * * // Swagger will generate a swagger spec for the Pets MVC routes. * use(Pets.class); * } * </pre> * * <p> * By default, Swagger will be mounted at <code>/swagger</code>, <code>/swagger/swagger.json</code> * and <code>/swagger/swagger.yml</code>. Go and try it! * </p> * * <p> * Or if you want to mount Swagger somewhere else...: * </p> * * <pre> * { * SwaggerUI.install("/api/docs", this)); * } * </pre> * * <p> * It is also possible to use Swagger (ui, .json or .yml) on specific resources. For example, * suppose we have a <code>Pets.java</code> resource mounted at <code>/pets</code>. The following * URL will be available too: * </p> * * <pre> * /swagger/pets (UI) * /swagger/pets/swagger.json (JSON) * /swagger/pets/swagger.yml (YML) * </pre> * * <p> * It is a small feature, but very useful if you have a medium-size API. * </p> * * <h2>swagger.conf</h2> * <p> * Jooby creates a {@link Swagger} model dynamically from MVC {@link RouteSpec}. But also, * defines some defaults inside the <code>swagger.conf</code> (see appendix). * </p> * * <p> * For example, <code>swagger.info.title</code> is set to <code>application.name</code>. If you want * to provide a more friendly name, description or API version... you can do it via your * <code>application.conf</code> file: * </p> * * <pre> * * swagger.info.title = My Awesome API * swagger.info.version = v0.1.0 * * </pre> * * @author edgar * @since 0.6.2 */ public class SwaggerUI { private static final Pattern TAG = Pattern.compile("(api)|/"); private Predicate<RouteSpec> predicate = r -> r.pattern().startsWith("/api"); private Function<RouteSpec, String> tag = r -> { Iterator<String> segments = Splitter.on(TAG) .trimResults() .omitEmptyStrings() .split(r.pattern()) .iterator(); if (segments.hasNext()) { return segments.next(); } return ""; }; private String path; private boolean ui = true; /** * Mount swagger at the given path. * * @param path A swagger path. */ public SwaggerUI(final String path) { this.path = requireNonNull(path, "Path is required."); } /** * Mount swagger at <code>/swagger</code>. */ public SwaggerUI() { this("/swagger"); } /** * Apply a route filter. By default, it keeps all the routes that starts with <code>/api</code>. * * @param predicate A filter to apply. * @return This instance. */ public SwaggerUI filter(final Predicate<RouteSpec> predicate) { this.predicate = requireNonNull(predicate, "Predicate is required."); return this; } /** * Creates a swagger tag from a route. The default tag provider extracts takes the first path * (ignoring <code>api</code>) from route pattern. For example, tag for <code>/api/pets</code> * will be <code>pets</code>. * * @param tag A tag provider. * @return This instance. */ public SwaggerUI tag(final Function<RouteSpec, String> tag) { this.tag = requireNonNull(tag, "Tag provider is required."); return this; } /** * Turn off swagger-ui. * * @return This instance. */ public SwaggerUI noUI() { ui = false; return this; } /** * Publish application routes as Swagger spec. * * @param app An application. */ public void install(final Jooby app) { requireNonNull(app, "Application is required."); ObjectMapper mapper = Json.mapper(); ObjectWriter yaml = Yaml.pretty(); app.use(new SwaggerModule(mapper)); app.get(path + "/swagger.json", path + "/:tag/swagger.json", req -> { SwaggerBuilder sb = req.require(SwaggerBuilder.class); Swagger swagger = sb.build(req.param("tag").toOptional(), predicate, tag, Swagger.class); byte[] json = mapper.writer().withDefaultPrettyPrinter().writeValueAsBytes(swagger); return Results.json(json); }).name("swagger(json)"); app.get(path + "/swagger.yml", path + "/:tag/swagger.yml", req -> { SwaggerBuilder sb = req.require(SwaggerBuilder.class); Swagger swagger = sb.build(req.param("tag").toOptional(), predicate, tag, SwaggerYml.class); byte[] yml = yaml.writeValueAsBytes(swagger); return Results.ok(yml).type("application/yaml"); }).name("swagger(yml)"); if (ui) { app.assets(path + "/ui/**", "/META-INF/resources/webjars/swagger-ui/" + wjversion(app.getClass()) + "/{0}"); app.get(path, path + "/:tag", new SwaggerHandler(path)) .name("swagger(html)") .produces(MediaType.html); } } private static String wjversion(final Class<?> loader) { try (InputStream in = loader.getResourceAsStream( "/META-INF/maven/org.webjars/swagger-ui/pom.properties")) { Properties properties = new Properties(); properties.load(in); return properties.getProperty("version"); } catch (IOException ex) { throw new IllegalStateException("No version found for swagger-ui", ex); } } }