Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added install script and local plug #315

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,8 @@ serve a SwaggerUI interface. The `path:` plug option must be supplied to give th

All JavaScript and CSS assets are sourced from cdnjs.cloudflare.com, rather than vendoring into this package.



```elixir
scope "/" do
pipe_through :browser # Use the default browser stack
Expand All @@ -299,6 +301,27 @@ All JavaScript and CSS assets are sourced from cdnjs.cloudflare.com, rather than
end
```

## Assets can be installed into the local application

```shell
mix openapi.spec.install 'install_path'
```
```elixir
scope "/" do
pipe_through :browser # Use the default browser stack

get "/", MyAppWeb.PageController, :index
get "/swaggerui", OpenApiSpex.Plug.SwaggerUI.Local, path: "/api/openapi"
end

scope "/api" do
pipe_through :api

resources "/users", MyAppWeb.UserController, only: [:create, :index, :show]
get "/openapi", OpenApiSpex.Plug.RenderSpec, []
end
```

## Importing an existing schema file

> :warning: This functionality currently converts Strings into Atoms, which makes it potentially [vulnerable to DoS attacks](https://til.hashrocket.com/posts/gkwwfy9xvw-converting-strings-to-atoms-safely). We recommend that you load Open API Schemas from _known files_ during application startup and _not dynamically from external sources at runtime_.
Expand Down
29 changes: 29 additions & 0 deletions lib/mix/tasks/install.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
defmodule Mix.Tasks.Openapi.Spec.Install do
@moduledoc """
Fetch and install the resources locally for the interface.

## Example

mix run Openapi.Install '/path_to_app'
"""
use Mix.Task

@preset 'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.32.4/swagger-ui-standalone-preset.js'
@css 'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.32.4/swagger-ui.css'
@bundle 'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.32.4/swagger-ui-bundle.js'

@impl true
def run([path]) do
Mix.Task.run("app.start")

:inets.start()
:ssl.start()

{:ok, :saved_to_file} = :httpc.request(:get, {css, []}, [], [stream: path <> '/css/'])
{:ok, :saved_to_file} = :httpc.request(:get, {@preset, []}, [], [stream: path <> '/js/'])
{:ok, :saved_to_file} = :httpc.request(:get, {@bundle, []}, [], [stream: path <> '/js/'])

end


end
151 changes: 151 additions & 0 deletions lib/open_api_spex/plug/swagger_ui_local.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
defmodule OpenApiSpex.Plug.SwaggerUI.Local do
@moduledoc """
Module plug that serves SwaggerUI.

The full path to the API spec must be given as a plug option.
The API spec should be served at the given path, see `OpenApiSpex.Plug.RenderSpec`

## Configuring SwaggerUI

SwaggerUI can be configured through plug `opts`.
All options will be converted from `snake_case` to `camelCase` and forwarded to the `SwaggerUIBundle` constructor.
See the [swagger-ui configuration docs](https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/) for details.
Should dynamic configuration be required, the `config_url` option can be set to an API endpoint that will provide additional config.

## Example

scope "/" do
pipe_through :browser # Use the default browser stack

get "/", MyAppWeb.PageController, :index
get "/swaggerui", OpenApiSpex.Plug.SwaggerUI,
path: "/api/openapi",
default_model_expand_depth: 3,
display_operation_id: true
end

# Other scopes may use custom stacks.
scope "/api" do
pipe_through :api
resources "/users", MyAppWeb.UserController, only: [:index, :create, :show]
get "/openapi", OpenApiSpex.Plug.RenderSpec, :show
end
"""
@behaviour Plug

@html """
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="/css/swagger-ui.css" >
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body {
margin:0;
background: #fafafa;
}
</style>
</head>

<body>
<div id="swagger-ui"></div>

<script src="/js/swagger-ui-bundle.js" charset="UTF-8"> </script>
<script src="/js/swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
<script>
window.onload = function() {
// Begin Swagger UI call region
const api_spec_url = new URL(window.location);
api_spec_url.pathname = "<%= config.path %>";
api_spec_url.hash = "";
const ui = SwaggerUIBundle({
url: api_spec_url.href,
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
requestInterceptor: function(request){
request.headers["x-csrf-token"] = "<%= csrf_token %>";
return request;
}
<%= for {k, v} <- Map.drop(config, [:path]) do %>
, <%= camelize(k) %>: <%= OpenApiSpex.OpenApi.json_encoder().encode!(v) %>
<% end %>
})
// End Swagger UI call region
window.ui = ui
}
</script>
</body>
</html>
"""

@doc """
Initializes the plug.

## Options

* `:path` - Required. The URL path to the API definition.
* all other opts - forwarded to the `SwaggerUIBundle` constructor

## Example

get "/swaggerui", OpenApiSpex.Plug.SwaggerUI,
path: "/api/openapi",
default_model_expand_depth: 3,
display_operation_id: true
"""
@impl Plug
def init(opts) when is_list(opts) do
Map.new(opts)
end

@impl Plug
def call(conn, config) do
csrf_token = Plug.CSRFProtection.get_csrf_token()
html = render(config, csrf_token)

conn
|> Plug.Conn.put_resp_content_type("text/html")
|> Plug.Conn.send_resp(200, html)
end

require EEx

EEx.function_from_string(:defp, :render, @html, [
:config,
:csrf_token
])

defp camelize(identifier) do
identifier
|> to_string
|> String.split("_", parts: 2)
|> case do
[first] -> first
[first, rest] -> first <> Macro.camelize(rest)
end
end
end