aboutsummaryrefslogtreecommitdiff
path: root/src/ejabberd_doc.erl
diff options
context:
space:
mode:
authorEvgeny Khramtsov <ekhramtsov@process-one.net>2020-01-08 12:24:51 +0300
committerEvgeny Khramtsov <ekhramtsov@process-one.net>2020-01-08 12:24:51 +0300
commit97da380acd8e3ba0c9a9618a99c4106eca1d1576 (patch)
treecc2b8fb7e175afcaf182a3595735f6f9177ec3dc /src/ejabberd_doc.erl
parentUpdate deps (diff)
Generate ejabberd.yml.5 man page from source code directly
Several documentation callbacks (doc/0 and mod_doc/0) are implemented and `ejabberdctl man` command is added to generate a man page. Note that the command requires a2x to be installed (which is a part of asciidoc package).
Diffstat (limited to 'src/ejabberd_doc.erl')
-rw-r--r--src/ejabberd_doc.erl459
1 files changed, 459 insertions, 0 deletions
diff --git a/src/ejabberd_doc.erl b/src/ejabberd_doc.erl
new file mode 100644
index 000000000..b659149fa
--- /dev/null
+++ b/src/ejabberd_doc.erl
@@ -0,0 +1,459 @@
+%%%----------------------------------------------------------------------
+%%% File : ejabberd_doc.erl
+%%% Purpose : Options documentation generator
+%%%
+%%% ejabberd, Copyright (C) 2002-2019 ProcessOne
+%%%
+%%% This program is free software; you can redistribute it and/or
+%%% modify it under the terms of the GNU General Public License as
+%%% published by the Free Software Foundation; either version 2 of the
+%%% License, or (at your option) any later version.
+%%%
+%%% This program is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%%% General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License along
+%%% with this program; if not, write to the Free Software Foundation, Inc.,
+%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+%%%
+%%%----------------------------------------------------------------------
+-module(ejabberd_doc).
+
+%% API
+-export([man/0, man/1, have_a2x/0]).
+
+-include("translate.hrl").
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+man() ->
+ man(<<"en">>).
+
+man(Lang) when is_list(Lang) ->
+ man(list_to_binary(Lang));
+man(Lang) ->
+ {ModDoc, SubModDoc} =
+ lists:foldl(
+ fun(M, {Mods, SubMods} = Acc) ->
+ case lists:prefix("mod_", atom_to_list(M)) orelse
+ lists:prefix("Elixir.Mod", atom_to_list(M)) of
+ true ->
+ try M:mod_doc() of
+ #{desc := Descr} = Map ->
+ DocOpts = maps:get(opts, Map, []),
+ Example = maps:get(example, Map, []),
+ {[{M, Descr, DocOpts, #{example => Example}}|Mods], SubMods};
+ #{opts := DocOpts} ->
+ {ParentMod, Backend} = strip_backend_suffix(M),
+ {Mods, dict:append(ParentMod, {M, Backend, DocOpts}, SubMods)}
+ catch _:undef ->
+ case erlang:function_exported(
+ M, mod_options, 1) of
+ true ->
+ warn("module ~s is not documented", [M]);
+ false ->
+ ok
+ end,
+ Acc
+ end;
+ false ->
+ Acc
+ end
+ end, {[], dict:new()}, ejabberd_config:beams(all)),
+ Doc = lists:flatmap(
+ fun(M) ->
+ try M:doc()
+ catch _:undef -> []
+ end
+ end, ejabberd_config:callback_modules(all)),
+ Options =
+ ["TOP LEVEL OPTIONS",
+ "-----------------",
+ tr(Lang, ?T("This section describes top level options of ejabberd.")),
+ io_lib:nl()] ++
+ lists:flatmap(
+ fun(Opt) ->
+ opt_to_man(Lang, Opt, 1)
+ end, lists:keysort(1, Doc)),
+ ModDoc1 = lists:map(
+ fun({M, Descr, DocOpts, Ex}) ->
+ case dict:find(M, SubModDoc) of
+ {ok, Backends} ->
+ {M, Descr, DocOpts, Backends, Ex};
+ error ->
+ {M, Descr, DocOpts, [], Ex}
+ end
+ end, ModDoc),
+ ModOptions =
+ [io_lib:nl(),
+ "MODULES",
+ "-------",
+ "[[modules]]",
+ tr(Lang, ?T("This section describes options of all ejabberd modules.")),
+ io_lib:nl()] ++
+ lists:flatmap(
+ fun({M, Descr, DocOpts, Backends, Example}) ->
+ ModName = atom_to_list(M),
+ [io_lib:nl(),
+ ModName,
+ lists:duplicate(length(atom_to_list(M)), $~),
+ "[[" ++ ModName ++ "]]",
+ io_lib:nl()] ++
+ tr_multi(Lang, Descr) ++ [io_lib:nl()] ++
+ opts_to_man(Lang, [{M, '', DocOpts}|Backends]) ++
+ format_example(0, Lang, Example)
+ end, lists:keysort(1, ModDoc1)),
+ ListenOptions =
+ [io_lib:nl(),
+ "LISTENERS",
+ "-------",
+ "[[listeners]]",
+ tr(Lang, ?T("This section describes options of all ejabberd listeners.")),
+ io_lib:nl(),
+ "TODO"],
+ AsciiData =
+ [[unicode:characters_to_binary(Line), io_lib:nl()]
+ || Line <- man_header(Lang) ++ Options ++ [io_lib:nl()]
+ ++ ModOptions ++ ListenOptions ++ man_footer(Lang)],
+ warn_undocumented_modules(ModDoc1),
+ warn_undocumented_options(Doc),
+ write_man(AsciiData).
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+opts_to_man(Lang, [{_, _, []}]) ->
+ Text = tr(Lang, ?T("The module has no options.")),
+ [Text, io_lib:nl()];
+opts_to_man(Lang, Backends) ->
+ lists:flatmap(
+ fun({_, Backend, DocOpts}) when DocOpts /= [] ->
+ Text = if Backend == '' ->
+ tr(Lang, ?T("Available options"));
+ true ->
+ lists:flatten(
+ io_lib:format(
+ tr(Lang, ?T("Available options for '~s' backend")),
+ [Backend]))
+ end,
+ [Text ++ ":", lists:duplicate(length(Text)+1, $^)|
+ lists:flatmap(
+ fun(Opt) -> opt_to_man(Lang, Opt, 1) end,
+ lists:keysort(1, DocOpts))] ++ [io_lib:nl()];
+ (_) ->
+ []
+ end, Backends).
+
+opt_to_man(Lang, {Option, Options}, Level) ->
+ [format_option(Lang, Option, Options)|format_desc(Lang, Options)] ++
+ format_example(Level, Lang, Options);
+opt_to_man(Lang, {Option, Options, Children}, Level) ->
+ [format_option(Lang, Option, Options)|format_desc(Lang, Options)] ++
+ lists:append(
+ [[H ++ ":"|T]
+ || [H|T] <- lists:map(
+ fun(Opt) -> opt_to_man(Lang, Opt, Level+1) end,
+ lists:keysort(1, Children))]) ++
+ [io_lib:nl()|format_example(Level, Lang, Options)].
+
+format_option(Lang, Option, #{value := Val}) ->
+ "*" ++ atom_to_list(Option) ++ "*: 'pass:[" ++
+ tr(Lang, Val) ++ "]'::";
+format_option(_Lang, Option, #{}) ->
+ "*" ++ atom_to_list(Option) ++ "*::".
+
+format_desc(Lang, #{desc := Desc}) ->
+ tr_multi(Lang, Desc).
+
+format_example(Level, Lang, #{example := [_|_] = Example}) ->
+ case lists:all(fun is_list/1, Example) of
+ true ->
+ if Level == 0 ->
+ ["*Example*:",
+ "^^^^^^^^^^"];
+ true ->
+ ["+", "*Example*:", "+"]
+ end ++ format_yaml(Example);
+ false when Level == 0 ->
+ ["Examples:",
+ "^^^^^^^^^"] ++
+ lists:flatmap(
+ fun({Text, Lines}) ->
+ [tr(Lang, Text)] ++ format_yaml(Lines)
+ end, Example);
+ false ->
+ lists:flatmap(
+ fun(Block) ->
+ ["+", "''''", "+"|Block]
+ end,
+ lists:map(
+ fun({Text, Lines}) ->
+ [tr(Lang, Text), "+"] ++ format_yaml(Lines)
+ end, Example))
+ end;
+format_example(_, _, _) ->
+ [].
+
+format_yaml(Lines) ->
+ ["==========================",
+ "[source,yaml]",
+ "----"|Lines] ++
+ ["----",
+ "=========================="].
+
+man_header(Lang) ->
+ ["ejabberd.yml(5)",
+ "===============",
+ ":doctype: manpage",
+ ":version: " ++ binary_to_list(ejabberd_config:version()),
+ io_lib:nl(),
+ "NAME",
+ "----",
+ "ejabberd.yml - " ++ tr(Lang, ?T("main configuration file for ejabberd.")),
+ io_lib:nl(),
+ "SYNOPSIS",
+ "--------",
+ "ejabberd.yml",
+ io_lib:nl(),
+ "DESCRIPTION",
+ "-----------",
+ tr(Lang, ?T("The configuration file is written in "
+ "https://en.wikipedia.org/wiki/YAML[YAML] language.")),
+ io_lib:nl(),
+ tr(Lang, ?T("WARNING: YAML is indentation sensitive, so make sure you respect "
+ "indentation, or otherwise you will get pretty cryptic "
+ "configuration errors.")),
+ io_lib:nl(),
+ tr(Lang, ?T("Logically, configuration options are splitted into 3 main categories: "
+ "'Modules', 'Listeners' and everything else called 'Top Level' options. "
+ "Thus this document is splitted into 3 main chapters describing each "
+ "category separately. So, the contents of ejabberd.yml will typically "
+ "look like this:")),
+ io_lib:nl(),
+ "==========================",
+ "[source,yaml]",
+ "----",
+ "hosts:",
+ " - example.com",
+ " - domain.tld",
+ "loglevel: info",
+ "...",
+ "listen:",
+ " -",
+ " port: 5222",
+ " module: ejabberd_c2s",
+ " ...",
+ "modules:",
+ " mod_roster: {}",
+ " ...",
+ "----",
+ "==========================",
+ io_lib:nl(),
+ tr(Lang, ?T("Any configuration error (such as syntax error, unknown option "
+ "or invalid option value) is fatal in the sense that ejabberd will "
+ "refuse to load the whole configuration file and will not start or will "
+ "abort configuration reload.")),
+ io_lib:nl(),
+ tr(Lang, ?T("All options can be changed in runtime by running 'ejabberdctl "
+ "reload-config' command. Configuration reload is atomic: either all options "
+ "are accepted and applied simultaneously or the new configuration is "
+ "refused without any impact on currently running configuration.")),
+ io_lib:nl(),
+ tr(Lang, ?T("Some options can be specified for particular virtual host(s) only "
+ "using 'host_config' or 'append_host_config' options. Such options "
+ "are called 'local'. Examples are 'modules', 'auth_method' and 'default_db'. "
+ "The options that cannot be defined per virtual host are called 'global'. "
+ "Examples are 'loglevel', 'certfiles' and 'listen'. It is a configuration "
+ "mistake to put 'global' options under 'host_config' or 'append_host_config' "
+ "section - ejabberd will refuse to load such configuration.")),
+ io_lib:nl(),
+ str:format(
+ tr(Lang, ?T("It is not recommended to write ejabberd.yml from scratch. Instead it is "
+ "better to start from \"default\" configuration file available at ~s. "
+ "Once you get ejabberd running you can start changing configuration "
+ "options to meet your requirements.")),
+ [default_config_url()]),
+ io_lib:nl(),
+ str:format(
+ tr(Lang, ?T("Note that this document is intended to provide comprehensive description of "
+ "all configuration options that can be consulted to understand the meaning "
+ "of a particular option, its format and possible values. It will be quite "
+ "hard to understand how to configure ejabberd by reading this document only "
+ "- for this purpose the reader is recommended to read online Configuration "
+ "Guide available at ~s.")),
+ [configuration_guide_url()]),
+ io_lib:nl()].
+
+man_footer(Lang) ->
+ {Year, _, _} = date(),
+ [io_lib:nl(),
+ "AUTHOR",
+ "------",
+ "https://www.process-one.net[ProcessOne].",
+ io_lib:nl(),
+ "VERSION",
+ "-------",
+ str:format(
+ tr(Lang, ?T("This document describes the configuration file of ejabberd ~ts. "
+ "Configuration options of other ejabberd versions "
+ "may differ significantly.")),
+ [ejabberd_config:version()]),
+ io_lib:nl(),
+ "REPORTING BUGS",
+ "--------------",
+ tr(Lang, ?T("Report bugs to <https://github.com/processone/ejabberd/issues>")),
+ io_lib:nl(),
+ "SEE ALSO",
+ "---------",
+ tr(Lang, ?T("Default configuration file")) ++ ": " ++ default_config_url(),
+ io_lib:nl(),
+ tr(Lang, ?T("Main site")) ++ ": <https://ejabberd.im>",
+ io_lib:nl(),
+ tr(Lang, ?T("Documentation")) ++ ": <https://docs.ejabberd.im>",
+ io_lib:nl(),
+ tr(Lang, ?T("Configuration Guide")) ++ ": " ++ configuration_guide_url(),
+ io_lib:nl(),
+ tr(Lang, ?T("Source code")) ++ ": <https://github.com/processone/ejabberd>",
+ io_lib:nl(),
+ "COPYING",
+ "-------",
+ "Copyright (c) 2002-" ++ integer_to_list(Year) ++
+ " https://www.process-one.net[ProcessOne]."].
+
+tr(Lang, {Format, Args}) ->
+ unicode:characters_to_list(
+ str:format(
+ translate:translate(Lang, iolist_to_binary(Format)),
+ Args));
+tr(Lang, Txt) ->
+ unicode:characters_to_list(translate:translate(Lang, iolist_to_binary(Txt))).
+
+tr_multi(Lang, Txt) when is_binary(Txt) ->
+ tr_multi(Lang, [Txt]);
+tr_multi(Lang, {Format, Args}) ->
+ tr_multi(Lang, [{Format, Args}]);
+tr_multi(Lang, Lines) when is_list(Lines) ->
+ [tr(Lang, Txt) || Txt <- Lines].
+
+write_man(AsciiData) ->
+ case file:get_cwd() of
+ {ok, Cwd} ->
+ AsciiDocFile = filename:join(Cwd, "ejabberd.yml.5.txt"),
+ ManPage = filename:join(Cwd, "ejabberd.yml.5"),
+ case file:write_file(AsciiDocFile, AsciiData) of
+ ok ->
+ Ret = run_a2x(Cwd, AsciiDocFile),
+ %%file:delete(AsciiDocFile),
+ case Ret of
+ ok ->
+ {ok, lists:flatten(
+ io_lib:format(
+ "The manpage saved as ~ts", [ManPage]))};
+ {error, Error} ->
+ {error, lists:flatten(
+ io_lib:format(
+ "Failed to generate manpage: ~ts", [Error]))}
+ end;
+ {error, Reason} ->
+ {error, lists:flatten(
+ io_lib:format(
+ "Failed to write to ~ts: ~s",
+ [AsciiDocFile, file:format_error(Reason)]))}
+ end;
+ {error, Reason} ->
+ {error, lists:flatten(
+ io_lib:format("Failed to get current directory: ~s",
+ [file:format_error(Reason)]))}
+ end.
+
+have_a2x() ->
+ case os:find_executable("a2x") of
+ false -> false;
+ Path -> {true, Path}
+ end.
+
+run_a2x(Cwd, AsciiDocFile) ->
+ case have_a2x() of
+ false ->
+ {error, "a2x was not found: do you have 'asciidoc' installed?"};
+ {true, Path} ->
+ Cmd = lists:flatten(
+ io_lib:format("~ts -f manpage ~ts -D ~ts",
+ [Path, AsciiDocFile, Cwd])),
+ case os:cmd(Cmd) of
+ "" -> ok;
+ Ret -> {error, Ret}
+ end
+ end.
+
+warn_undocumented_modules(Docs) ->
+ lists:foreach(
+ fun({M, _, DocOpts, Backends, _}) ->
+ warn_undocumented_module(M, DocOpts),
+ lists:foreach(
+ fun({SubM, _, SubOpts}) ->
+ warn_undocumented_module(SubM, SubOpts)
+ end, Backends)
+ end, Docs).
+
+warn_undocumented_module(M, DocOpts) ->
+ try M:mod_options(ejabberd_config:get_myname()) of
+ Defaults ->
+ lists:foreach(
+ fun(OptDefault) ->
+ Opt = case OptDefault of
+ O when is_atom(O) -> O;
+ {O, _} -> O
+ end,
+ case lists:keymember(Opt, 1, DocOpts) of
+ false ->
+ warn("~s: option ~s is not documented",
+ [M, Opt]);
+ true ->
+ ok
+ end
+ end, Defaults)
+ catch _:undef ->
+ ok
+ end.
+
+warn_undocumented_options(Docs) ->
+ Opts = lists:flatmap(
+ fun(M) ->
+ try M:options() of
+ Defaults ->
+ lists:map(
+ fun({O, _}) -> O;
+ (O) when is_atom(O) -> O
+ end, Defaults)
+ catch _:undef ->
+ []
+ end
+ end, ejabberd_config:callback_modules(all)),
+ lists:foreach(
+ fun(Opt) ->
+ case lists:keymember(Opt, 1, Docs) of
+ false ->
+ warn("option ~s is not documented", [Opt]);
+ true ->
+ ok
+ end
+ end, Opts).
+
+warn(Format, Args) ->
+ io:format(standard_error, "Warning: " ++ Format ++ "~n", Args).
+
+strip_backend_suffix(M) ->
+ [H|T] = lists:reverse(string:tokens(atom_to_list(M), "_")),
+ {list_to_atom(string:join(lists:reverse(T), "_")), list_to_atom(H)}.
+
+default_config_url() ->
+ "<https://github.com/processone/ejabberd/blob/" ++
+ binary_to_list(binary:part(ejabberd_config:version(), {0,5})) ++
+ "/ejabberd.yml.example>".
+
+configuration_guide_url() ->
+ "<https://docs.ejabberd.im/admin/configuration>".