Skip to content

Commit

Permalink
Support grouping options in docs by section
Browse files Browse the repository at this point in the history
  • Loading branch information
pnezis committed Jul 10, 2024
1 parent e4be67b commit 0c86c8a
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 32 deletions.
6 changes: 6 additions & 0 deletions cli_options/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

### Added

* Support grouping options in the docs by section. You can now specify the `:doc_section`
to any option, and pass a `:sections` option in the `CliOptions.docs/2` function for the
headers and extended docs of each section.

## [v0.1.1](https://github.com/sportradar/elixir-workspace/tree/cli_options/v0.1.1) (2024-07-04)

### Added
Expand Down
23 changes: 23 additions & 0 deletions cli_options/lib/cli_options.ex
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,29 @@ defmodule CliOptions do
## Options
* `:sort` - if set to `true` the options will be sorted alphabetically.
* `:sections` - a keyword list with options sections. If set the options docs
will be added under the defined section, or at the root section if no
`:section` is defined in your schema.
Notice that if `:sort` is set the options
will be sorted within the sections. The sections order is not sorted and it
follows the provided order.
An entry for each section is expected in the `:sections` option with the
following format:
[
section_name: [
header: "Section Header",
doc: "Optional extra docs for this docs section"
]
]
where:
* `:header` - The header that will be used for the section. Required.
* `:doc` - Optional detailed section docs to be added before the actual
options docs.
"""
@spec docs(schema :: keyword() | CliOptions.Schema.t(), opts :: keyword()) :: String.t()
def docs(schema, opts \\ [])
Expand Down
135 changes: 106 additions & 29 deletions cli_options/lib/cli_options/docs.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,119 @@ defmodule CliOptions.Docs do

@doc false
@spec generate(schema :: CliOptions.Schema.t(), opts :: keyword()) :: String.t()
def generate(%CliOptions.Schema{} = schema, opts) do
schema.schema
def generate(%CliOptions.Schema{schema: schema}, opts) do
validate_sections!(schema, opts[:sections])
sections = opts[:sections] || []

schema
|> remove_hidden_options()
|> group_by_section([nil] ++ Keyword.keys(sections))
|> maybe_sort(Keyword.get(opts, :sort, false))
|> Enum.reduce([], &maybe_option_doc/2)
|> Enum.reverse()
|> Enum.join("\n")
|> Enum.map(fn {section, options} -> docs_by_section(section, options, sections) end)
|> Enum.join("\n\n")
end

@sections_schema NimbleOptions.new!(
*: [
type: :keyword_list,
keys: [
header: [
type: :string,
required: true
],
doc: [type: :string]
]
]
)

defp validate_sections!(_schema, nil), do: :ok

defp validate_sections!(schema, sections) do
sections = NimbleOptions.validate!(sections, @sections_schema)

configured_sections =
schema
|> Enum.map(fn {_key, opts} -> opts[:doc_section] end)
|> Enum.reject(&is_nil/1)

for section <- configured_sections do
if is_nil(sections[section]) do
raise ArgumentError, """
You must include #{inspect(section)} in the :sections option
of CliOptions.docs/2, as following:
sections: [
#{section}: [
header: "The section header",
doc: "Optional extended doc for the section"
]
]
"""
end
end
end

defp remove_hidden_options(schema),
do: Enum.reject(schema, fn {_key, opts} -> opts[:doc] == false end)

defp maybe_sort(schema, true), do: Enum.sort_by(schema, fn {key, _value} -> key end, :asc)
defp maybe_sort(schema, _other), do: schema

defp maybe_option_doc({key, schema}, acc) do
option_doc({key, schema}, acc)
end

defp option_doc({_key, schema}, acc) do
doc =
[
"*",
"`#{option_name_doc(schema)}`",
"(`#{schema[:type]}`)",
"-",
maybe_deprecated(schema),
maybe_required(schema),
option_body_doc(schema)
]
|> Enum.filter(&is_binary/1)
|> Enum.map(&String.trim_trailing/1)
|> Enum.join(" ")
|> String.trim_trailing()

[doc | acc]
defp group_by_section(schema, [nil]), do: [nil: schema]

defp group_by_section(schema, sections) do
sections
|> Enum.reduce([], fn section, acc ->
options = Enum.filter(schema, fn {_key, opts} -> opts[:doc_section] == section end)

case options do
[] -> acc
options -> [{section, options} | acc]
end
end)
|> Enum.reverse()
end

defp maybe_sort(sections, true) do
Enum.map(sections, fn {section, options} ->
sorted = Enum.sort_by(options, fn {key, _value} -> key end, :asc)
{section, sorted}
end)
end

defp maybe_sort(sections, _other), do: sections

defp docs_by_section(nil, options, _sections), do: options_docs(options)

defp docs_by_section(section, options, sections) do
section_opts = Keyword.fetch!(sections, section)

[
"### " <> Keyword.fetch!(section_opts, :header),
section_opts[:doc],
options_docs(options)
]
|> Enum.reject(&is_nil/1)
|> Enum.join("\n\n")
end

defp options_docs(options) do
options
|> Enum.map(fn {_key, schema} -> option_doc(schema) end)
|> Enum.join("\n")
end

defp option_doc(schema) do
[
"*",
"`#{option_name_doc(schema)}`",
"(`#{schema[:type]}`)",
"-",
maybe_deprecated(schema),
maybe_required(schema),
option_body_doc(schema)
]
|> Enum.filter(&is_binary/1)
|> Enum.map(&String.trim_trailing/1)
|> Enum.join(" ")
|> String.trim_trailing()
end

defp option_name_doc(schema) do
Expand Down
8 changes: 8 additions & 0 deletions cli_options/lib/cli_options/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@ defmodule CliOptions.Schema do
then the option will not be included in the generated docs.
"""
],
doc_section: [
type: :atom,
doc: """
The section in the documentation this option will be put under. If not set the
option is added to the default unnamed section. If set you must also provide the
`:sections` option in the `CliOptions.docs/2` call.
"""
],
required: [
type: :boolean,
doc: """
Expand Down
75 changes: 73 additions & 2 deletions cli_options/test/cli_options/docs_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ defmodule CliOptions.DocsTest do
mode: [
type: :string,
default: "parallel",
allowed: ["parallel", "serial"]
allowed: ["parallel", "serial"],
doc_section: :test
],
with_dash: [
type: :boolean,
doc: "a key with a dash"
doc: "a key with a dash",
doc_section: :test
],
hidden_option: [
type: :boolean,
Expand All @@ -41,6 +43,75 @@ defmodule CliOptions.DocsTest do
assert CliOptions.docs(@test_schema) == expected
end

test "with sections configured" do
expected =
"""
* `--verbose` (`boolean`) - [default: `false`]
* `-p, --project...` (`string`) - Required. The project to use
### Test related options
* `--mode` (`string`) - Allowed values: `["parallel", "serial"]`. [default: `parallel`]
* `--with-dash` (`boolean`) - a key with a dash [default: `false`]
"""
|> String.trim()

assert CliOptions.docs(@test_schema, sections: [test: [header: "Test related options"]]) ==
expected

# extra sections are ignored if no option set

assert CliOptions.docs(@test_schema,
sections: [test: [header: "Test related options"], other: [header: "Foo"]]
) == expected
end

test "raises with invalid section settings" do
message =
"unknown options [:heder], valid options are: [:header, :doc] (in options [:test])"

assert_raise NimbleOptions.ValidationError, message, fn ->
CliOptions.docs(@test_schema, sections: [test: [heder: "Test related options"]])
end
end

test "raises if no section info is provided for an option" do
message = """
You must include :foo in the :sections option
of CliOptions.docs/2, as following:
sections: [
foo: [
header: "The section header",
doc: "Optional extended doc for the section"
]
]
"""

schema = [var: [doc: "a var", long: "variable", doc_section: :foo]]

assert_raise ArgumentError, message, fn -> CliOptions.docs(schema, sections: []) end
end

test "with sections configured and sorting" do
expected =
"""
* `-p, --project...` (`string`) - Required. The project to use
* `--verbose` (`boolean`) - [default: `false`]
### Test related options
* `--mode` (`string`) - Allowed values: `["parallel", "serial"]`. [default: `parallel`]
* `--with-dash` (`boolean`) - a key with a dash [default: `false`]
"""
|> String.trim()

assert CliOptions.docs(@test_schema,
sort: true,
sections: [test: [header: "Test related options"]]
) == expected
end

test "with sorting enabled" do
expected =
"""
Expand Down
2 changes: 1 addition & 1 deletion cli_options/test/cli_options/schema_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ defmodule CliOptions.SchemaTest do

message =
"invalid schema for :foo, unknown options [:missing, :other], valid options are: " <>
"[:type, :default, :long, :short, :aliases, :short_aliases, :doc, :required, :multiple, " <>
"[:type, :default, :long, :short, :aliases, :short_aliases, :doc, :doc_section, :required, :multiple, " <>
":allowed, :deprecated]"

assert_raise ArgumentError, message, fn ->
Expand Down

0 comments on commit 0c86c8a

Please sign in to comment.