Merge branch 'develop' into feature/account-export
[akkoma] / lib / pleroma / docs / generator.ex
index aa578eee280657f4a7bf9eb35016f58bfbc795e9..a70f83b73a83e6f8c2d7ff5e5e6933d8fb408703 100644 (file)
@@ -1,3 +1,7 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
 defmodule Pleroma.Docs.Generator do
   @callback process(keyword()) :: {:ok, String.t()}
 
@@ -6,68 +10,127 @@ defmodule Pleroma.Docs.Generator do
     implementation.process(descriptions)
   end
 
-  @spec uploaders_list() :: [module()]
-  def uploaders_list do
-    {:ok, modules} = :application.get_key(:pleroma, :modules)
+  @spec list_behaviour_implementations(behaviour :: module()) :: [module()]
+  def list_behaviour_implementations(behaviour) do
+    :code.all_loaded()
+    |> Enum.filter(fn {module, _} ->
+      # This shouldn't be needed as all modules are expected to have module_info/1,
+      # but in test enviroments some transient modules `:elixir_compiler_XX`
+      # are loaded for some reason (where XX is a random integer).
+      if function_exported?(module, :module_info, 1) do
+        module.module_info(:attributes)
+        |> Keyword.get_values(:behaviour)
+        |> List.flatten()
+        |> Enum.member?(behaviour)
+      end
+    end)
+    |> Enum.map(fn {module, _} -> module end)
+  end
 
-    Enum.filter(modules, fn module ->
-      name_as_list = Module.split(module)
+  @doc """
+  Converts:
+  - atoms to strings with leading `:`
+  - module names to strings, without leading `Elixir.`
+  - add humanized labels to `keys` if label is not defined, e.g. `:instance` -> `Instance`
+  """
+  @spec convert_to_strings([map()]) :: [map()]
+  def convert_to_strings(descriptions) do
+    Enum.map(descriptions, &format_entity(&1))
+  end
 
-      List.starts_with?(name_as_list, ["Pleroma", "Uploaders"]) and
-        List.last(name_as_list) != "Uploader"
-    end)
+  defp format_entity(entity) do
+    entity
+    |> format_key()
+    |> Map.put(:group, atom_to_string(entity[:group]))
+    |> format_children()
   end
 
-  @spec filters_list() :: [module()]
-  def filters_list do
-    {:ok, modules} = :application.get_key(:pleroma, :modules)
+  defp format_key(%{key: key} = entity) do
+    entity
+    |> Map.put(:key, atom_to_string(key))
+    |> Map.put(:label, entity[:label] || humanize(key))
+  end
 
-    Enum.filter(modules, fn module ->
-      name_as_list = Module.split(module)
+  defp format_key(%{group: group} = entity) do
+    Map.put(entity, :label, entity[:label] || humanize(group))
+  end
 
-      List.starts_with?(name_as_list, ["Pleroma", "Upload", "Filter"])
-    end)
+  defp format_key(entity), do: entity
+
+  defp format_children(%{children: children} = entity) do
+    Map.put(entity, :children, Enum.map(children, &format_child(&1)))
   end
 
-  @spec mrf_list() :: [module()]
-  def mrf_list do
-    {:ok, modules} = :application.get_key(:pleroma, :modules)
+  defp format_children(entity), do: entity
 
-    Enum.filter(modules, fn module ->
-      name_as_list = Module.split(module)
+  defp format_child(%{suggestions: suggestions} = entity) do
+    entity
+    |> Map.put(:suggestions, format_suggestions(suggestions))
+    |> format_key()
+    |> format_group()
+    |> format_children()
+  end
 
-      List.starts_with?(name_as_list, ["Pleroma", "Web", "ActivityPub", "MRF"]) and
-        length(name_as_list) > 4
-    end)
+  defp format_child(entity) do
+    entity
+    |> format_key()
+    |> format_group()
+    |> format_children()
+  end
+
+  defp format_group(%{group: group} = entity) do
+    Map.put(entity, :group, format_suggestion(group))
   end
 
-  @spec richmedia_parsers() :: [module()]
-  def richmedia_parsers do
-    {:ok, modules} = :application.get_key(:pleroma, :modules)
+  defp format_group(entity), do: entity
 
-    Enum.filter(modules, fn module ->
-      name_as_list = Module.split(module)
+  defp atom_to_string(entity) when is_binary(entity), do: entity
 
-      List.starts_with?(name_as_list, ["Pleroma", "Web", "RichMedia", "Parsers"]) and
-        length(name_as_list) == 5
-    end)
+  defp atom_to_string(entity) when is_atom(entity), do: inspect(entity)
+
+  defp humanize(entity) do
+    string = inspect(entity)
+
+    if String.starts_with?(string, ":"),
+      do: Phoenix.Naming.humanize(entity),
+      else: string
   end
+
+  defp format_suggestions({:list_behaviour_implementations, behaviour}) do
+    behaviour
+    |> list_behaviour_implementations()
+    |> format_suggestions()
+  end
+
+  defp format_suggestions([]), do: []
+
+  defp format_suggestions([suggestion | tail]) do
+    [format_suggestion(suggestion) | format_suggestions(tail)]
+  end
+
+  defp format_suggestion(entity) when is_atom(entity) do
+    atom_to_string(entity)
+  end
+
+  defp format_suggestion([head | tail] = entity) when is_list(entity) do
+    [format_suggestion(head) | format_suggestions(tail)]
+  end
+
+  defp format_suggestion(entity) when is_tuple(entity) do
+    format_suggestions(Tuple.to_list(entity)) |> List.to_tuple()
+  end
+
+  defp format_suggestion(entity), do: entity
 end
 
 defimpl Jason.Encoder, for: Tuple do
-  def encode(tuple, opts) do
-    Jason.Encode.list(Tuple.to_list(tuple), opts)
-  end
+  def encode(tuple, opts), do: Jason.Encode.list(Tuple.to_list(tuple), opts)
 end
 
 defimpl Jason.Encoder, for: [Regex, Function] do
-  def encode(term, opts) do
-    Jason.Encode.string(inspect(term), opts)
-  end
+  def encode(term, opts), do: Jason.Encode.string(inspect(term), opts)
 end
 
 defimpl String.Chars, for: Regex do
-  def to_string(term) do
-    inspect(term)
-  end
+  def to_string(term), do: inspect(term)
 end