From 4347d2de5eb609bbfa1a206a5de5df925d3a0696 Mon Sep 17 00:00:00 2001
From: href <href@random.sh>
Date: Sun, 12 Jul 2020 17:23:33 +0200
Subject: [PATCH] Config/Docs: Expand behaviour suggestions at runtime

---
 config/description.exs                        | 14 ++-------
 lib/pleroma/application.ex                    |  1 +
 lib/pleroma/docs/generator.ex                 | 31 +++++++++++++------
 lib/pleroma/docs/json.ex                      | 21 ++++++++-----
 lib/pleroma/docs/markdown.ex                  |  5 +++
 .../controllers/config_controller.ex          |  4 +--
 test/docs/generator_test.exs                  | 12 ++-----
 7 files changed, 47 insertions(+), 41 deletions(-)

diff --git a/config/description.exs b/config/description.exs
index b0cc8d527..61d1d055e 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -23,18 +23,14 @@
         key: :uploader,
         type: :module,
         description: "Module which will be used for uploads",
-        suggestions: [Pleroma.Uploaders.Local, Pleroma.Uploaders.S3]
+        suggestions: {:list_behaviour_implementations, Pleroma.Uploaders.Uploader}
       },
       %{
         key: :filters,
         type: {:list, :module},
         description:
           "List of filter modules for uploads. Module names are shortened (removed leading `Pleroma.Upload.Filter.` part), but on adding custom module you need to use full name.",
-        suggestions:
-          Generator.list_modules_in_dir(
-            "lib/pleroma/upload/filter",
-            "Elixir.Pleroma.Upload.Filter."
-          )
+        suggestions: {:list_behaviour_implementations, Pleroma.Upload.Filter}
       },
       %{
         key: :link_name,
@@ -1404,11 +1400,7 @@
         type: [:module, {:list, :module}],
         description:
           "A list of MRF policies enabled. Module names are shortened (removed leading `Pleroma.Web.ActivityPub.MRF.` part), but on adding custom module you need to use full name.",
-        suggestions:
-          Generator.list_modules_in_dir(
-            "lib/pleroma/web/activity_pub/mrf",
-            "Elixir.Pleroma.Web.ActivityPub.MRF."
-          )
+        suggestions: {:list_behaviour_implementations, Pleroma.Web.ActivityPub.MRF}
       },
       %{
         key: :transparency,
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index 84f3aa82d..b68a373a4 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -42,6 +42,7 @@ def start(_type, _args) do
     Pleroma.ApplicationRequirements.verify!()
     setup_instrumenters()
     load_custom_modules()
+    Pleroma.Docs.JSON.compile()
 
     adapter = Application.get_env(:tesla, :adapter)
 
diff --git a/lib/pleroma/docs/generator.ex b/lib/pleroma/docs/generator.ex
index e0fc8cd02..a671a6278 100644
--- a/lib/pleroma/docs/generator.ex
+++ b/lib/pleroma/docs/generator.ex
@@ -6,16 +6,21 @@ def process(implementation, descriptions) do
     implementation.process(descriptions)
   end
 
-  @spec list_modules_in_dir(String.t(), String.t()) :: [module()]
-  def list_modules_in_dir(dir, start) do
-    with {:ok, files} <- File.ls(dir) do
-      files
-      |> Enum.filter(&String.ends_with?(&1, ".ex"))
-      |> Enum.map(fn filename ->
-        module = filename |> String.trim_trailing(".ex") |> Macro.camelize()
-        String.to_atom(start <> module)
-      end)
-    end
+  @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
 
   @doc """
@@ -87,6 +92,12 @@ defp humanize(entity) do
       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
diff --git a/lib/pleroma/docs/json.ex b/lib/pleroma/docs/json.ex
index d1cf1f487..feeb4320e 100644
--- a/lib/pleroma/docs/json.ex
+++ b/lib/pleroma/docs/json.ex
@@ -1,5 +1,19 @@
 defmodule Pleroma.Docs.JSON do
   @behaviour Pleroma.Docs.Generator
+  @external_resource "config/description.exs"
+  @raw_config Pleroma.Config.Loader.read("config/description.exs")
+  @raw_descriptions @raw_config[:pleroma][:config_description]
+  @term __MODULE__.Compiled
+
+  @spec compile :: :ok
+  def compile do
+    :persistent_term.put(@term, Pleroma.Docs.Generator.convert_to_strings(@raw_descriptions))
+  end
+
+  @spec compiled_descriptions :: Map.t()
+  def compiled_descriptions do
+    :persistent_term.get(@term)
+  end
 
   @spec process(keyword()) :: {:ok, String.t()}
   def process(descriptions) do
@@ -13,11 +27,4 @@ def process(descriptions) do
       {:ok, path}
     end
   end
-
-  def compile do
-    with config <- Pleroma.Config.Loader.read("config/description.exs") do
-      config[:pleroma][:config_description]
-      |> Pleroma.Docs.Generator.convert_to_strings()
-    end
-  end
 end
diff --git a/lib/pleroma/docs/markdown.ex b/lib/pleroma/docs/markdown.ex
index 68b106499..da3f20f43 100644
--- a/lib/pleroma/docs/markdown.ex
+++ b/lib/pleroma/docs/markdown.ex
@@ -68,6 +68,11 @@ defp print_suggestion(file, suggestion, as_list \\ false) do
     IO.write(file, "  #{list_mark}`#{inspect(suggestion)}`\n")
   end
 
+  defp print_suggestions(file, {:list_behaviour_implementations, behaviour}) do
+    suggestions = Pleroma.Docs.Generator.list_behaviour_implementations(behaviour)
+    print_suggestions(file, suggestions)
+  end
+
   defp print_suggestions(_file, nil), do: nil
 
   defp print_suggestions(_file, ""), do: nil
diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex
index 7f60470cb..0df13007f 100644
--- a/lib/pleroma/web/admin_api/controllers/config_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/config_controller.ex
@@ -9,8 +9,6 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
   alias Pleroma.ConfigDB
   alias Pleroma.Plugs.OAuthScopesPlug
 
-  @descriptions Pleroma.Docs.JSON.compile()
-
   plug(Pleroma.Web.ApiSpec.CastAndValidate)
   plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action == :update)
 
@@ -25,7 +23,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
   defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ConfigOperation
 
   def descriptions(conn, _params) do
-    descriptions = Enum.filter(@descriptions, &whitelisted_config?/1)
+    descriptions = Enum.filter(Pleroma.Docs.JSON.compiled_descriptions(), &whitelisted_config?/1)
 
     json(conn, descriptions)
   end
diff --git a/test/docs/generator_test.exs b/test/docs/generator_test.exs
index 9c9f4357b..b32918a69 100644
--- a/test/docs/generator_test.exs
+++ b/test/docs/generator_test.exs
@@ -13,21 +13,13 @@ defmodule Pleroma.Docs.GeneratorTest do
           key: :uploader,
           type: :module,
           description: "",
-          suggestions:
-            Generator.list_modules_in_dir(
-              "lib/pleroma/upload/filter",
-              "Elixir.Pleroma.Upload.Filter."
-            )
+          suggestions: {:list_behaviour_implementations, Pleroma.Upload.Filter}
         },
         %{
           key: :filters,
           type: {:list, :module},
           description: "",
-          suggestions:
-            Generator.list_modules_in_dir(
-              "lib/pleroma/web/activity_pub/mrf",
-              "Elixir.Pleroma.Web.ActivityPub.MRF."
-            )
+          suggestions: {:list_behaviour_implementations, Pleroma.Web.ActivityPub.MRF}
         },
         %{
           key: Pleroma.Upload,