diff --git a/lib/plug/redoc_ui.ex b/lib/plug/redoc_ui.ex index b03b4cf..fd9ac6c 100644 --- a/lib/plug/redoc_ui.ex +++ b/lib/plug/redoc_ui.ex @@ -1,4 +1,29 @@ defmodule Redoc.Plug.RedocUI do + @plug_options_schema [ + title: [ + type: :string, + default: "ReDoc", + doc: "Custom ReDoc site title." + ], + spec_url: [ + type: :string, + doc: "Open API spec URL.", + deprecated: "Use `:spec_url` in `:redoc_opts` instead." + ], + redoc_version: [ + type: :string, + default: "latest", + doc: "Specify ReDoc version" + ], + redoc_options: [ + type: :keyword_list, + keys: Redoc.options_schema(), + doc: """ + ReDoc options. See https://github.com/Redocly/redoc#redoc-options-object for more details. + """ + ] + ] + @moduledoc """ A Plug for rendering Redoc UI. @@ -15,9 +40,7 @@ defmodule Redoc.Plug.RedocUI do ## Options - * `spec_url` - The openapi path to fetch. Support both `yaml` and `json`. - * `redoc_version` - Specify a Redoc version, default to `latest`. - * `title` - The documentation HTML page title, default to `ReDoc`. + #{NimbleOptions.docs(@plug_options_schema)} """ @behaviour Plug @@ -42,8 +65,8 @@ defmodule Redoc.Plug.RedocUI do - <%= k %>="<%= v %>" + <%= for {k, v} <- redoc_options do %> + <%= k %>="<%= v %>" <% end %> > @@ -51,55 +74,23 @@ defmodule Redoc.Plug.RedocUI do """ - @redoc_options [ - :disable_search, - :download_definition_url, - :download_file_name, - :expand_default_server_variables, - :expand_responses, - :expand_single_schema_field, - :generated_payload_samples_max_depth, - :hide_download_button, - :hide_fab, - :hide_hostname, - :hide_loading, - :hide_schema_pattern, - :hide_schema_titles, - :hide_single_request_sample_tab, - :json_sample_expand_level, - :lazy_rendering, - :max_displayed_enum_values, - :menu_toggle, - :min_character_length_to_init_search, - :native_scrollbars, - :nonce, - :only_required_in_samples, - :path_in_middle_panel, - :payload_sample_idx, - :required_props_first, - :schema_expansion_level, - :scroll_y_offset, - :show_extensions, - :show_object_schema_examples, - :show_webhook_ver, - :side_nav_style, - :simple_one_of_type_label, - :sort_enum_values_alphabetically, - :sort_operations_alphabetically, - :sort_props_alphabetically, - :sort_tags_alphabetically, - :spec_url, - :theme, - :untrusted_spec - ] - @impl true def init(opts) do - redoc_opts = encode_options(opts) - redoc_version = Keyword.get(opts, :redoc_version, "latest") - title = Keyword.get(opts, :title, "ReDoc") + case NimbleOptions.validate(opts, @plug_options_schema) do + {:ok, opts} -> + # Honor spec_url in the root of the `opts`. + redoc_options = + (opts[:redoc_options] || []) + |> maybe_merge_spec_url(opts[:spec_url]) + |> encode_options() - [redoc_opts: redoc_opts, redoc_version: redoc_version, title: title] + opts + |> Keyword.delete(:redoc_options) + |> Keyword.put(:redoc_options, redoc_options) + + otherwise -> + otherwise + end end @impl true @@ -109,9 +100,14 @@ defmodule Redoc.Plug.RedocUI do |> send_resp(200, EEx.eval_string(@index_html, opts)) end - defp encode_options(opts) do - Keyword.take(opts, @redoc_options) - |> Enum.map(fn {key, value} -> {to_kebab_case(key), value} end) + defp maybe_merge_spec_url(redoc_options, nil), do: redoc_options + + defp maybe_merge_spec_url(redoc_options, spec_url), + do: Keyword.merge(redoc_options, spec_url: spec_url) + + # TODO: need test. + defp encode_options(redoc_options) do + Enum.map(redoc_options, fn {key, value} -> {to_kebab_case(key), value} end) end defp to_kebab_case(identifier) do diff --git a/lib/redoc.ex b/lib/redoc.ex index 1a70aa4..f58608f 100644 --- a/lib/redoc.ex +++ b/lib/redoc.ex @@ -5,4 +5,140 @@ defmodule Redoc do Please consult documentation in `Redoc.Plug.RedocUI` for more details. """ + + @doc """ + A list of ReDoc options schema. + """ + # NOTE: lazy_rendering doesn't add to this list since ReDoc documentation said it + # it doesn't implement it yet. + def options_schema() do + [ + disable_search: [ + type: :boolean, + default: false + ], + min_character_length_to_init_search: [ + type: :integer, + default: 3 + ], + expand_default_server_variables: [ + type: :boolean, + default: false + ], + expand_responses: [ + type: :string, + default: "all" + ], + generated_payload_samples_max_depth: [ + type: :integer, + default: 10 + ], + max_displayed_enum_values: [ + type: :integer + ], + hide_download_button: [ + type: :boolean, + default: true + ], + download_file_name: [ + type: :string + ], + download_definition_url: [ + type: :string + ], + hide_hostname: [ + type: :boolean + ], + hide_loading: [ + type: :boolean + ], + hide_fab: [ + type: :boolean + ], + hide_schema_pattern: [ + type: :boolean + ], + hide_single_request_sample_tab: [ + type: :boolean + ], + show_object_schema_examples: [ + type: :boolean, + default: false + ], + expand_single_schema_field: [ + type: :boolean + ], + schema_expansion_level: [ + type: {:or, [:integer, :string]}, + default: 0 + ], + json_sample_expand_level: [ + type: {:or, [:integer, :string]}, + default: 0 + ], + hide_schema_titles: [ + type: :boolean + ], + simple_one_of_type_label: [ + type: :boolean + ], + sort_enum_values_alphabetically: [ + type: :boolean + ], + sort_operations_alphabetically: [ + type: :boolean + ], + sort_tags_alphabetically: [ + type: :boolean + ], + menu_toggle: [ + type: :boolean, + default: true + ], + native_scrollbars: [ + type: :boolean + ], + only_required_in_samples: [ + type: :boolean + ], + path_in_middle_panel: [ + type: :boolean + ], + required_props_first: [ + type: :boolean + ], + # NOTE: function not support. + scroll_y_offset: [ + type: {:or, [:integer, :string]} + ], + show_extensions: [ + type: {:or, [:integer, :string, {:list, :string}]} + ], + sort_props_alphabetically: [ + type: :boolean + ], + payload_sample_idx: [ + type: :integer + ], + # TODO: revisit theme object. + # theme: [ + # type: :keyword_list + # ] + untrusted_spec: [ + type: :boolean + ], + nonce: [ + type: :string + ], + side_nav_style: [ + type: {:in, [:"summary-only", :"path-only", :"id-only"]} + ], + show_webhook_ver: [ + type: :boolean + ], + spec_url: [ + type: :string + ] + ] + end end diff --git a/mix.exs b/mix.exs index fdd1d22..246786e 100644 --- a/mix.exs +++ b/mix.exs @@ -25,6 +25,7 @@ defmodule Redoc.MixProject do [ {:plug, "~> 1.0"}, {:jason, "~> 1.0"}, + {:nimble_options, "~> 0.3"}, {:ex_doc, "~> 0.28", only: :dev, runtime: false}, ] end diff --git a/mix.lock b/mix.lock index 67d5079..16f0a80 100644 --- a/mix.lock +++ b/mix.lock @@ -6,6 +6,7 @@ "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, "mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"}, + "nimble_options": {:hex, :nimble_options, "0.5.2", "42703307b924880f8c08d97719da7472673391905f528259915782bb346e0a1b", [:mix], [], "hexpm", "4da7f904b915fd71db549bcdc25f8d56f378ef7ae07dc1d372cbe72ba950dce0"}, "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, "open_api_spex": {:hex, :open_api_spex, "3.11.0", "e4ea9c00e2891c3195c2df511c49e280e7210272ba0077d8d9975258c70e43c0", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.1 or ~> 4.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "640bd09f6bdd96cc0264213bfa229b0f8b3868df3384311de57e0f3fa22fc3af"}, "plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"},