/**
* 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.raml;
import static java.util.Objects.requireNonNull;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.jooby.Env;
import org.jooby.Jooby;
import org.jooby.MediaType;
import org.jooby.Results;
import org.jooby.Route;
import org.jooby.internal.raml.RamlBuilder;
import org.jooby.spec.RouteProcessor;
import org.jooby.spec.RouteSpec;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.io.ByteStreams;
import com.google.inject.Binder;
import com.google.inject.TypeLiteral;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
/**
* <h1>raml</h1>
* <p>
* RESTful API Modeling Language (RAML) makes it easy to manage the whole API lifecycle from design
* to sharing. It's concise - you only write what you need to define - and reusable. It is machine
* readable API design that is actually human friendly. More at
* <a href="http://raml.org/">http://raml.org</a>
* </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>usage</h2>
*
* <pre>
* {
* // define your API... via script or MVC:
* /{@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;
* })
* ...;
*
* new Raml().install(this);
* }
* </pre>
*
* <p>
* You also need the <code>jooby:spec</code> maven plugin:
* </p>
*
* <pre>
* <plugin>
* <groupId>org.jooby</groupId>
* <artifactId>jooby-maven-plugin</artifactId>
* <executions>
* <execution>
* <goals>
* <goal>spec</goal>
* </goals>
* </execution>
* </executions>
* </plugin>
* </pre>
*
* <p>
* The plugin compiles the API and produces a <code>.spec</code> file for prod environments.
* </p>
*
* <p>
* The RAML api-console will be available at <code>/raml</code> and the <code>.raml</code> will be
* at: <code>/raml/api.raml</code>
* </p>
*
* <h2>options</h2>
* <p>
* There are a few options available, let's see what they are:
* </p>
*
* <h3>path</h3>
* <p>
* The <code>path</code> option controls where to mount the RAML routes:
* </p>
* <pre>
* {
* ...
* new Raml("docs").install(this);
* }
* </pre>
*
* <p>
* Produces: <code>/docs</code> for api-console and <code>/docs/api.raml</code>. Default path is:
* <code>/raml</code>.
* </p>
*
* <h3>filter</h3>
* <p>
* The <code>filter</code> option controls what is exported to <a href="http://raml.org">RAML</a>:
* </p>
*
* <pre>
* {
* ...
* new Raml()
* .filter(route {@literal ->} {
* return route.pattern().startsWith("/api");
* })
* .install(this);
* }
* </pre>
*
* <p>
* Default filter keeps <code>/api/*</code> routes.
* </p>
*
* <h3>noConsole</h3>
* <p>
* This option turn off the api-console:
* </p>
*
* <pre>
* {
* ...
* new Raml()
* .noConsole()
* .install(this);
* }
* </pre>
*
* <h3>theme</h3>
* <p>
* Set the ui-theme for api-console. Available options are <code>light</code> and <code>dark</code>.
* Default is: <code>light</code>.
* </p>
*
* <pre>
* {
* ...
* new Raml()
* .theme("dark")
* .install(this);
* }
* </pre>
*
* <h3>clientGenerator</h3>
* <p>
* Shows/hide the client generator button from api-console.
* </p>
*
* <h3>tryIt</h3>
* <p>
* Expand/collapse the try it panel from api-console.
* </p>
*
* @author edgar
* @since 0.15.0
*/
public class Raml {
private static final String DISABLE_TRY_IT = "disable-try-it";
private static final String DISABLE_RAML_CLIENT_GENERATOR = "disable-raml-client-generator";
private static final String DISABLE_THEME_SWITCHER = "disable-theme-switcher";
private static final String RAML = "/api.raml";
private static final TypeLiteral<Set<Route.Definition>> ROUTES = new TypeLiteral<Set<Route.Definition>>() {
};
private String path;
private Predicate<RouteSpec> filter;
private String template;
private Set<String> options = Sets.newHashSet(
DISABLE_THEME_SWITCHER,
DISABLE_RAML_CLIENT_GENERATOR);
private boolean console = true;
private String theme = "light";
/**
* Creates a new {@link Raml}.
*
* @param path A raml path.
*/
public Raml(final String path) {
this.path = path;
filter = r -> r.pattern().startsWith("/api") && !r.method().equals("*");
this.template = read("index.html");
}
/**
* Creates a new {@link Raml} under the <code>/raml</code> path.
*/
public Raml() {
this("/raml");
}
/**
* Apply a route filter. By default only routes at <code>/api</code> will be exported.
*
* @param filter A route filter.
* @return This instance.
*/
public Raml filter(final Predicate<RouteSpec> filter) {
this.filter = requireNonNull(filter, "Filter is required.");
return this;
}
/**
* Set a ui-theme for api-console.
*
* @param theme Dark or light. Default is light.
* @return This instance.
*/
public Raml theme(final String theme) {
this.theme = requireNonNull(theme, "Theme is required.");
return this;
}
/**
* Shows/hide client generator button for api-console.
*
* @param enabled True shows the button.
* @return This instance.
*/
public Raml clientGenerator(final boolean enabled) {
if (enabled) {
options.remove(DISABLE_RAML_CLIENT_GENERATOR);
} else {
options.add(DISABLE_RAML_CLIENT_GENERATOR);
}
return this;
}
/**
* Expand/collapse the try-it panel for api-console.
*
* @param enabled True expands the panel.
* @return This instance.
*/
public Raml tryIt(final boolean enabled) {
if (enabled) {
options.remove(DISABLE_TRY_IT);
} else {
options.add(DISABLE_TRY_IT);
}
return this;
}
/**
* Turn off api-console.
*
* @return This instance.
*/
public Raml noConsole() {
this.console = false;
return this;
}
/**
* Install {@link Raml} in the given app.
*
* @param app An application.
*/
public void install(final Jooby app) {
app.use(conf());
app.get(path + RAML, path + "/:tag" + RAML, req -> {
Set<Route.Definition> routes = req.require(ROUTES);
Predicate<RouteSpec> predicate = req.param("tag").toOptional().map(t -> {
return this.filter.and(r -> r.pattern().contains("/" + t));
}).orElse(this.filter);
RouteProcessor processor = new RouteProcessor();
List<RouteSpec> specs = processor.process(app.getClass(), Lists.newArrayList(routes))
.stream()
.filter(predicate)
.collect(Collectors.toList());
RamlBuilder raml = req.require(RamlBuilder.class);
return Results.ok(raml.build(specs)).type(MediaType.text);
}).name("raml")
.produces(MediaType.text);
// console
if (console) {
app.assets(path + "/static/**", "/org/jooby/raml/dist/{0}").name("api-console/static");
app.get(path, path + "/:tag", req -> {
Config conf = req.require(Config.class);
String name = conf.getString("raml.title");
String raml = req.param("tag").toOptional()
.map(t -> path + "/" + t + RAML)
.orElse(path + RAML);
String html = template
.replace("${name}", name)
.replace("${theme}", theme)
.replace("${path}", path)
.replace("${raml}", "." + raml)
.replace("${opts}", options.stream().collect(Collectors.joining(" ")));
return Results.ok(html).type(MediaType.html);
}).name("api-console")
.produces(MediaType.html);
}
}
private String read(final String name) {
try (InputStream stream = getClass().getResourceAsStream(name)) {
return new String(ByteStreams.toByteArray(stream), "UTF-8");
} catch (IOException ex) {
throw Throwables.propagate(ex);
}
}
private static Jooby.Module conf() {
return new Jooby.Module() {
@Override
public void configure(final Env env, final Config conf, final Binder binder) {
}
@Override
public Config config() {
return ConfigFactory.parseResources(Raml.class, "raml.conf");
}
};
}
}