From 7e7a3e15449792581412be002f287c504e3449a6 Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Tue, 14 Apr 2020 18:36:32 +0400
Subject: [PATCH 1/3] Add OpenAPI spec for FilterController

---
 lib/pleroma/filter.ex                         |  9 +-
 .../api_spec/operations/filter_operation.ex   | 89 +++++++++++++++++++
 lib/pleroma/web/api_spec/schemas/filter.ex    | 51 +++++++++++
 .../api_spec/schemas/filter_create_request.ex | 30 +++++++
 .../api_spec/schemas/filter_update_request.ex | 41 +++++++++
 .../web/api_spec/schemas/filters_response.ex  | 40 +++++++++
 .../controllers/filter_controller.ex          | 54 +++++------
 .../web/mastodon_api/views/filter_view.ex     |  6 +-
 test/filter_test.exs                          | 10 +--
 .../controllers/filter_controller_test.exs    | 55 ++++++++++--
 10 files changed, 340 insertions(+), 45 deletions(-)
 create mode 100644 lib/pleroma/web/api_spec/operations/filter_operation.ex
 create mode 100644 lib/pleroma/web/api_spec/schemas/filter.ex
 create mode 100644 lib/pleroma/web/api_spec/schemas/filter_create_request.ex
 create mode 100644 lib/pleroma/web/api_spec/schemas/filter_update_request.ex
 create mode 100644 lib/pleroma/web/api_spec/schemas/filters_response.ex

diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex
index 7cb49360f..4d61b3650 100644
--- a/lib/pleroma/filter.ex
+++ b/lib/pleroma/filter.ex
@@ -89,11 +89,10 @@ defmodule Pleroma.Filter do
     |> Repo.delete()
   end
 
-  def update(%Pleroma.Filter{} = filter) do
-    destination = Map.from_struct(filter)
-
-    Pleroma.Filter.get(filter.filter_id, %{id: filter.user_id})
-    |> cast(destination, [:phrase, :context, :hide, :expires_at, :whole_word])
+  def update(%Pleroma.Filter{} = filter, params) do
+    filter
+    |> cast(params, [:phrase, :context, :hide, :expires_at, :whole_word])
+    |> validate_required([:phrase, :context])
     |> Repo.update()
   end
 end
diff --git a/lib/pleroma/web/api_spec/operations/filter_operation.ex b/lib/pleroma/web/api_spec/operations/filter_operation.ex
new file mode 100644
index 000000000..0d673f566
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/filter_operation.ex
@@ -0,0 +1,89 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.FilterOperation do
+  alias OpenApiSpex.Operation
+  alias OpenApiSpex.Schema
+  alias Pleroma.Web.ApiSpec.Helpers
+  alias Pleroma.Web.ApiSpec.Schemas.Filter
+  alias Pleroma.Web.ApiSpec.Schemas.FilterCreateRequest
+  alias Pleroma.Web.ApiSpec.Schemas.FiltersResponse
+  alias Pleroma.Web.ApiSpec.Schemas.FilterUpdateRequest
+
+  def open_api_operation(action) do
+    operation = String.to_existing_atom("#{action}_operation")
+    apply(__MODULE__, operation, [])
+  end
+
+  def index_operation do
+    %Operation{
+      tags: ["apps"],
+      summary: "View all filters",
+      operationId: "FilterController.index",
+      security: [%{"oAuth" => ["read:filters"]}],
+      responses: %{
+        200 => Operation.response("Filters", "application/json", FiltersResponse)
+      }
+    }
+  end
+
+  def create_operation do
+    %Operation{
+      tags: ["apps"],
+      summary: "Create a filter",
+      operationId: "FilterController.create",
+      requestBody: Helpers.request_body("Parameters", FilterCreateRequest, required: true),
+      security: [%{"oAuth" => ["write:filters"]}],
+      responses: %{200 => Operation.response("Filter", "application/json", Filter)}
+    }
+  end
+
+  def show_operation do
+    %Operation{
+      tags: ["apps"],
+      summary: "View all filters",
+      parameters: [id_param()],
+      operationId: "FilterController.show",
+      security: [%{"oAuth" => ["read:filters"]}],
+      responses: %{
+        200 => Operation.response("Filter", "application/json", Filter)
+      }
+    }
+  end
+
+  def update_operation do
+    %Operation{
+      tags: ["apps"],
+      summary: "Update a filter",
+      parameters: [id_param()],
+      operationId: "FilterController.update",
+      requestBody: Helpers.request_body("Parameters", FilterUpdateRequest, required: true),
+      security: [%{"oAuth" => ["write:filters"]}],
+      responses: %{
+        200 => Operation.response("Filter", "application/json", Filter)
+      }
+    }
+  end
+
+  def delete_operation do
+    %Operation{
+      tags: ["apps"],
+      summary: "Remove a filter",
+      parameters: [id_param()],
+      operationId: "FilterController.delete",
+      security: [%{"oAuth" => ["write:filters"]}],
+      responses: %{
+        200 =>
+          Operation.response("Filter", "application/json", %Schema{
+            type: :object,
+            description: "Empty object"
+          })
+      }
+    }
+  end
+
+  defp id_param do
+    Operation.parameter(:id, :path, :string, "Filter ID", example: "123", required: true)
+  end
+end
diff --git a/lib/pleroma/web/api_spec/schemas/filter.ex b/lib/pleroma/web/api_spec/schemas/filter.ex
new file mode 100644
index 000000000..fc5480b71
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/filter.ex
@@ -0,0 +1,51 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.Filter do
+  alias OpenApiSpex.Schema
+  require OpenApiSpex
+
+  OpenApiSpex.schema(%{
+    title: "Filter",
+    type: :object,
+    properties: %{
+      id: %Schema{type: :string},
+      phrase: %Schema{type: :string, description: "The text to be filtered"},
+      context: %Schema{
+        type: :array,
+        items: %Schema{type: :string, enum: ["home", "notifications", "public", "thread"]},
+        description: "The contexts in which the filter should be applied."
+      },
+      expires_at: %Schema{
+        type: :string,
+        format: :"date-time",
+        description:
+          "When the filter should no longer be applied. String (ISO 8601 Datetime), or null if the filter does not expire.",
+        nullable: true
+      },
+      irreversible: %Schema{
+        type: :boolean,
+        description:
+          "Should matching entities in home and notifications be dropped by the server?"
+      },
+      whole_word: %Schema{
+        type: :boolean,
+        description: "Should the filter consider word boundaries?"
+      }
+    },
+    example: %{
+      "id" => "5580",
+      "phrase" => "@twitter.com",
+      "context" => [
+        "home",
+        "notifications",
+        "public",
+        "thread"
+      ],
+      "whole_word" => false,
+      "expires_at" => nil,
+      "irreversible" => true
+    }
+  })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/filter_create_request.ex b/lib/pleroma/web/api_spec/schemas/filter_create_request.ex
new file mode 100644
index 000000000..f2a475b12
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/filter_create_request.ex
@@ -0,0 +1,30 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.FilterCreateRequest do
+  alias OpenApiSpex.Schema
+  require OpenApiSpex
+
+  OpenApiSpex.schema(%{
+    title: "FilterCreateRequest",
+    allOf: [
+      %OpenApiSpex.Reference{"$ref": "#/components/schemas/FilterUpdateRequest"},
+      %Schema{
+        type: :object,
+        properties: %{
+          irreversible: %Schema{
+            type: :bolean,
+            description:
+              "Should the server irreversibly drop matching entities from home and notifications?",
+            default: false
+          }
+        }
+      }
+    ],
+    example: %{
+      "phrase" => "knights",
+      "context" => ["home"]
+    }
+  })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/filter_update_request.ex b/lib/pleroma/web/api_spec/schemas/filter_update_request.ex
new file mode 100644
index 000000000..e703db0ce
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/filter_update_request.ex
@@ -0,0 +1,41 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.FilterUpdateRequest do
+  alias OpenApiSpex.Schema
+  require OpenApiSpex
+
+  OpenApiSpex.schema(%{
+    title: "FilterUpdateRequest",
+    type: :object,
+    properties: %{
+      phrase: %Schema{type: :string, description: "The text to be filtered"},
+      context: %Schema{
+        type: :array,
+        items: %Schema{type: :string, enum: ["home", "notifications", "public", "thread"]},
+        description:
+          "Array of enumerable strings `home`, `notifications`, `public`, `thread`. At least one context must be specified."
+      },
+      irreversible: %Schema{
+        type: :bolean,
+        description:
+          "Should the server irreversibly drop matching entities from home and notifications?"
+      },
+      whole_word: %Schema{type: :bolean, description: "Consider word boundaries?", default: true}
+      # TODO: probably should implement filter expiration
+      # expires_in: %Schema{
+      #   type: :string,
+      #   format: :"date-time",
+      #   description:
+      #     "ISO 8601 Datetime for when the filter expires. Otherwise,
+      #  null for a filter that doesn't expire."
+      # }
+    },
+    required: [:phrase, :context],
+    example: %{
+      "phrase" => "knights",
+      "context" => ["home"]
+    }
+  })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/filters_response.ex b/lib/pleroma/web/api_spec/schemas/filters_response.ex
new file mode 100644
index 000000000..8c56c5982
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/filters_response.ex
@@ -0,0 +1,40 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.FiltersResponse do
+  require OpenApiSpex
+  alias Pleroma.Web.ApiSpec.Schemas.Filter
+
+  OpenApiSpex.schema(%{
+    title: "FiltersResponse",
+    description: "Array of Filters",
+    type: :array,
+    items: Filter,
+    example: [
+      %{
+        "id" => "5580",
+        "phrase" => "@twitter.com",
+        "context" => [
+          "home",
+          "notifications",
+          "public",
+          "thread"
+        ],
+        "whole_word" => false,
+        "expires_at" => nil,
+        "irreversible" => true
+      },
+      %{
+        "id" => "6191",
+        "phrase" => ":eurovision2019:",
+        "context" => [
+          "home"
+        ],
+        "whole_word" => true,
+        "expires_at" => "2019-05-21T13:47:31.333Z",
+        "irreversible" => false
+      }
+    ]
+  })
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
index 7fd0562c9..dd13a8a09 100644
--- a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
@@ -10,67 +10,69 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
 
   @oauth_read_actions [:show, :index]
 
+  plug(Pleroma.Web.ApiSpec.CastAndValidate)
   plug(OAuthScopesPlug, %{scopes: ["read:filters"]} when action in @oauth_read_actions)
 
   plug(
     OAuthScopesPlug,
     %{scopes: ["write:filters"]} when action not in @oauth_read_actions
   )
+  
+  defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.FilterOperation
 
   @doc "GET /api/v1/filters"
   def index(%{assigns: %{user: user}} = conn, _) do
     filters = Filter.get_filters(user)
 
-    render(conn, "filters.json", filters: filters)
+    render(conn, "index.json", filters: filters)
   end
 
   @doc "POST /api/v1/filters"
-  def create(
-        %{assigns: %{user: user}} = conn,
-        %{"phrase" => phrase, "context" => context} = params
-      ) do
+  def create(%{assigns: %{user: user}, body_params: params} = conn, _) do
     query = %Filter{
       user_id: user.id,
-      phrase: phrase,
-      context: context,
-      hide: Map.get(params, "irreversible", false),
-      whole_word: Map.get(params, "boolean", true)
+      phrase: params.phrase,
+      context: params.context,
+      hide: params.irreversible,
+      whole_word: params.whole_word
       # expires_at
     }
 
     {:ok, response} = Filter.create(query)
 
-    render(conn, "filter.json", filter: response)
+    render(conn, "show.json", filter: response)
   end
 
   @doc "GET /api/v1/filters/:id"
-  def show(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
+  def show(%{assigns: %{user: user}} = conn, %{id: filter_id}) do
     filter = Filter.get(filter_id, user)
 
-    render(conn, "filter.json", filter: filter)
+    render(conn, "show.json", filter: filter)
   end
 
   @doc "PUT /api/v1/filters/:id"
   def update(
-        %{assigns: %{user: user}} = conn,
-        %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
+        %{assigns: %{user: user}, body_params: params} = conn,
+        %{id: filter_id}
       ) do
-    query = %Filter{
-      user_id: user.id,
-      filter_id: filter_id,
-      phrase: phrase,
-      context: context,
-      hide: Map.get(params, "irreversible", nil),
-      whole_word: Map.get(params, "boolean", true)
-      # expires_at
-    }
+    params =
+      params
+      |> Map.from_struct()
+      |> Map.delete(:irreversible)
+      |> Map.put(:hide, params.irreversible)
+      |> Enum.reject(fn {_key, value} -> is_nil(value) end)
+      |> Map.new()
 
-    {:ok, response} = Filter.update(query)
-    render(conn, "filter.json", filter: response)
+    # TODO: add expires_in -> expires_at
+
+    with %Filter{} = filter <- Filter.get(filter_id, user),
+         {:ok, %Filter{} = filter} <- Filter.update(filter, params) do
+      render(conn, "show.json", filter: filter)
+    end
   end
 
   @doc "DELETE /api/v1/filters/:id"
-  def delete(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
+  def delete(%{assigns: %{user: user}} = conn, %{id: filter_id}) do
     query = %Filter{
       user_id: user.id,
       filter_id: filter_id
diff --git a/lib/pleroma/web/mastodon_api/views/filter_view.ex b/lib/pleroma/web/mastodon_api/views/filter_view.ex
index 97fd1e83f..8d5c381ec 100644
--- a/lib/pleroma/web/mastodon_api/views/filter_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/filter_view.ex
@@ -7,11 +7,11 @@ defmodule Pleroma.Web.MastodonAPI.FilterView do
   alias Pleroma.Web.CommonAPI.Utils
   alias Pleroma.Web.MastodonAPI.FilterView
 
-  def render("filters.json", %{filters: filters} = opts) do
-    render_many(filters, FilterView, "filter.json", opts)
+  def render("index.json", %{filters: filters} = opts) do
+    render_many(filters, FilterView, "show.json", opts)
   end
 
-  def render("filter.json", %{filter: filter}) do
+  def render("show.json", %{filter: filter}) do
     expires_at =
       if filter.expires_at do
         Utils.to_masto_date(filter.expires_at)
diff --git a/test/filter_test.exs b/test/filter_test.exs
index b2a8330ee..63a30c736 100644
--- a/test/filter_test.exs
+++ b/test/filter_test.exs
@@ -141,17 +141,15 @@ defmodule Pleroma.FilterTest do
       context: ["home"]
     }
 
-    query_two = %Pleroma.Filter{
-      user_id: user.id,
-      filter_id: 1,
+    changes = %{
       phrase: "who",
       context: ["home", "timeline"]
     }
 
     {:ok, filter_one} = Pleroma.Filter.create(query_one)
-    {:ok, filter_two} = Pleroma.Filter.update(query_two)
+    {:ok, filter_two} = Pleroma.Filter.update(filter_one, changes)
     assert filter_one != filter_two
-    assert filter_two.phrase == query_two.phrase
-    assert filter_two.context == query_two.context
+    assert filter_two.phrase == changes.phrase
+    assert filter_two.context == changes.context
   end
 end
diff --git a/test/web/mastodon_api/controllers/filter_controller_test.exs b/test/web/mastodon_api/controllers/filter_controller_test.exs
index 97ab005e0..41a290eb2 100644
--- a/test/web/mastodon_api/controllers/filter_controller_test.exs
+++ b/test/web/mastodon_api/controllers/filter_controller_test.exs
@@ -5,8 +5,15 @@
 defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
   use Pleroma.Web.ConnCase
 
+  alias Pleroma.Web.ApiSpec
+  alias Pleroma.Web.ApiSpec.Schemas.Filter
+  alias Pleroma.Web.ApiSpec.Schemas.FilterCreateRequest
+  alias Pleroma.Web.ApiSpec.Schemas.FiltersResponse
+  alias Pleroma.Web.ApiSpec.Schemas.FilterUpdateRequest
   alias Pleroma.Web.MastodonAPI.FilterView
 
+  import OpenApiSpex.TestAssertions
+
   test "creating a filter" do
     %{conn: conn} = oauth_access(["write:filters"])
 
@@ -15,7 +22,10 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
       context: ["home"]
     }
 
-    conn = post(conn, "/api/v1/filters", %{"phrase" => filter.phrase, context: filter.context})
+    conn =
+      conn
+      |> put_req_header("content-type", "application/json")
+      |> post("/api/v1/filters", %{"phrase" => filter.phrase, context: filter.context})
 
     assert response = json_response(conn, 200)
     assert response["phrase"] == filter.phrase
@@ -23,6 +33,7 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
     assert response["irreversible"] == false
     assert response["id"] != nil
     assert response["id"] != ""
+    assert_schema(response, "Filter", ApiSpec.spec())
   end
 
   test "fetching a list of filters" do
@@ -53,9 +64,11 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
     assert response ==
              render_json(
                FilterView,
-               "filters.json",
+               "index.json",
                filters: [filter_two, filter_one]
              )
+
+    assert_schema(response, "FiltersResponse", ApiSpec.spec())
   end
 
   test "get a filter" do
@@ -72,7 +85,8 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
 
     conn = get(conn, "/api/v1/filters/#{filter.filter_id}")
 
-    assert _response = json_response(conn, 200)
+    assert response = json_response(conn, 200)
+    assert_schema(response, "Filter", ApiSpec.spec())
   end
 
   test "update a filter" do
@@ -82,7 +96,8 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
       user_id: user.id,
       filter_id: 2,
       phrase: "knight",
-      context: ["home"]
+      context: ["home"],
+      hide: true
     }
 
     {:ok, _filter} = Pleroma.Filter.create(query)
@@ -93,7 +108,9 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
     }
 
     conn =
-      put(conn, "/api/v1/filters/#{query.filter_id}", %{
+      conn
+      |> put_req_header("content-type", "application/json")
+      |> put("/api/v1/filters/#{query.filter_id}", %{
         phrase: new.phrase,
         context: new.context
       })
@@ -101,6 +118,8 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
     assert response = json_response(conn, 200)
     assert response["phrase"] == new.phrase
     assert response["context"] == new.context
+    assert response["irreversible"] == true
+    assert_schema(response, "Filter", ApiSpec.spec())
   end
 
   test "delete a filter" do
@@ -120,4 +139,30 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
     assert response = json_response(conn, 200)
     assert response == %{}
   end
+
+  describe "OpenAPI" do
+    test "Filter example matches schema" do
+      api_spec = ApiSpec.spec()
+      schema = Filter.schema()
+      assert_schema(schema.example, "Filter", api_spec)
+    end
+
+    test "FiltersResponse example matches schema" do
+      api_spec = ApiSpec.spec()
+      schema = FiltersResponse.schema()
+      assert_schema(schema.example, "FiltersResponse", api_spec)
+    end
+
+    test "FilterCreateRequest example matches schema" do
+      api_spec = ApiSpec.spec()
+      schema = FilterCreateRequest.schema()
+      assert_schema(schema.example, "FilterCreateRequest", api_spec)
+    end
+
+    test "FilterUpdateRequest example matches schema" do
+      api_spec = ApiSpec.spec()
+      schema = FilterUpdateRequest.schema()
+      assert_schema(schema.example, "FilterUpdateRequest", api_spec)
+    end
+  end
 end

From 46aae346f8530d4b9933b8e718e9578a96447f0a Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Mon, 27 Apr 2020 23:54:11 +0400
Subject: [PATCH 2/3] Move single used schemas to Filter operation schema

---
 .../api_spec/operations/filter_operation.ex   | 158 ++++++++++++++++--
 lib/pleroma/web/api_spec/schemas/filter.ex    |  51 ------
 .../api_spec/schemas/filter_create_request.ex |  30 ----
 .../api_spec/schemas/filter_update_request.ex |  41 -----
 .../web/api_spec/schemas/filters_response.ex  |  40 -----
 .../controllers/filter_controller.ex          |   7 +-
 .../web/mastodon_api/views/filter_view.ex     |   4 +-
 .../controllers/filter_controller_test.exs    |  49 +-----
 8 files changed, 158 insertions(+), 222 deletions(-)
 delete mode 100644 lib/pleroma/web/api_spec/schemas/filter.ex
 delete mode 100644 lib/pleroma/web/api_spec/schemas/filter_create_request.ex
 delete mode 100644 lib/pleroma/web/api_spec/schemas/filter_update_request.ex
 delete mode 100644 lib/pleroma/web/api_spec/schemas/filters_response.ex

diff --git a/lib/pleroma/web/api_spec/operations/filter_operation.ex b/lib/pleroma/web/api_spec/operations/filter_operation.ex
index 0d673f566..53e57b46b 100644
--- a/lib/pleroma/web/api_spec/operations/filter_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/filter_operation.ex
@@ -6,10 +6,6 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do
   alias OpenApiSpex.Operation
   alias OpenApiSpex.Schema
   alias Pleroma.Web.ApiSpec.Helpers
-  alias Pleroma.Web.ApiSpec.Schemas.Filter
-  alias Pleroma.Web.ApiSpec.Schemas.FilterCreateRequest
-  alias Pleroma.Web.ApiSpec.Schemas.FiltersResponse
-  alias Pleroma.Web.ApiSpec.Schemas.FilterUpdateRequest
 
   def open_api_operation(action) do
     operation = String.to_existing_atom("#{action}_operation")
@@ -23,7 +19,7 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do
       operationId: "FilterController.index",
       security: [%{"oAuth" => ["read:filters"]}],
       responses: %{
-        200 => Operation.response("Filters", "application/json", FiltersResponse)
+        200 => Operation.response("Filters", "application/json", array_of_filters())
       }
     }
   end
@@ -33,9 +29,9 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do
       tags: ["apps"],
       summary: "Create a filter",
       operationId: "FilterController.create",
-      requestBody: Helpers.request_body("Parameters", FilterCreateRequest, required: true),
+      requestBody: Helpers.request_body("Parameters", create_request(), required: true),
       security: [%{"oAuth" => ["write:filters"]}],
-      responses: %{200 => Operation.response("Filter", "application/json", Filter)}
+      responses: %{200 => Operation.response("Filter", "application/json", filter())}
     }
   end
 
@@ -47,7 +43,7 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do
       operationId: "FilterController.show",
       security: [%{"oAuth" => ["read:filters"]}],
       responses: %{
-        200 => Operation.response("Filter", "application/json", Filter)
+        200 => Operation.response("Filter", "application/json", filter())
       }
     }
   end
@@ -58,10 +54,10 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do
       summary: "Update a filter",
       parameters: [id_param()],
       operationId: "FilterController.update",
-      requestBody: Helpers.request_body("Parameters", FilterUpdateRequest, required: true),
+      requestBody: Helpers.request_body("Parameters", update_request(), required: true),
       security: [%{"oAuth" => ["write:filters"]}],
       responses: %{
-        200 => Operation.response("Filter", "application/json", Filter)
+        200 => Operation.response("Filter", "application/json", filter())
       }
     }
   end
@@ -86,4 +82,146 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do
   defp id_param do
     Operation.parameter(:id, :path, :string, "Filter ID", example: "123", required: true)
   end
+
+  defp filter do
+    %Schema{
+      title: "Filter",
+      type: :object,
+      properties: %{
+        id: %Schema{type: :string},
+        phrase: %Schema{type: :string, description: "The text to be filtered"},
+        context: %Schema{
+          type: :array,
+          items: %Schema{type: :string, enum: ["home", "notifications", "public", "thread"]},
+          description: "The contexts in which the filter should be applied."
+        },
+        expires_at: %Schema{
+          type: :string,
+          format: :"date-time",
+          description:
+            "When the filter should no longer be applied. String (ISO 8601 Datetime), or null if the filter does not expire.",
+          nullable: true
+        },
+        irreversible: %Schema{
+          type: :boolean,
+          description:
+            "Should matching entities in home and notifications be dropped by the server?"
+        },
+        whole_word: %Schema{
+          type: :boolean,
+          description: "Should the filter consider word boundaries?"
+        }
+      },
+      example: %{
+        "id" => "5580",
+        "phrase" => "@twitter.com",
+        "context" => [
+          "home",
+          "notifications",
+          "public",
+          "thread"
+        ],
+        "whole_word" => false,
+        "expires_at" => nil,
+        "irreversible" => true
+      }
+    }
+  end
+
+  defp array_of_filters do
+    %Schema{
+      title: "ArrayOfFilters",
+      description: "Array of Filters",
+      type: :array,
+      items: filter(),
+      example: [
+        %{
+          "id" => "5580",
+          "phrase" => "@twitter.com",
+          "context" => [
+            "home",
+            "notifications",
+            "public",
+            "thread"
+          ],
+          "whole_word" => false,
+          "expires_at" => nil,
+          "irreversible" => true
+        },
+        %{
+          "id" => "6191",
+          "phrase" => ":eurovision2019:",
+          "context" => [
+            "home"
+          ],
+          "whole_word" => true,
+          "expires_at" => "2019-05-21T13:47:31.333Z",
+          "irreversible" => false
+        }
+      ]
+    }
+  end
+
+  defp create_request do
+    %Schema{
+      title: "FilterCreateRequest",
+      allOf: [
+        update_request(),
+        %Schema{
+          type: :object,
+          properties: %{
+            irreversible: %Schema{
+              type: :bolean,
+              description:
+                "Should the server irreversibly drop matching entities from home and notifications?",
+              default: false
+            }
+          }
+        }
+      ],
+      example: %{
+        "phrase" => "knights",
+        "context" => ["home"]
+      }
+    }
+  end
+
+  defp update_request do
+    %Schema{
+      title: "FilterUpdateRequest",
+      type: :object,
+      properties: %{
+        phrase: %Schema{type: :string, description: "The text to be filtered"},
+        context: %Schema{
+          type: :array,
+          items: %Schema{type: :string, enum: ["home", "notifications", "public", "thread"]},
+          description:
+            "Array of enumerable strings `home`, `notifications`, `public`, `thread`. At least one context must be specified."
+        },
+        irreversible: %Schema{
+          type: :bolean,
+          description:
+            "Should the server irreversibly drop matching entities from home and notifications?"
+        },
+        whole_word: %Schema{
+          type: :bolean,
+          description: "Consider word boundaries?",
+          default: true
+        }
+        # TODO: probably should implement filter expiration
+        # expires_in: %Schema{
+        #   type: :string,
+        #   format: :"date-time",
+        #   description:
+        #     "ISO 8601 Datetime for when the filter expires. Otherwise,
+        #  null for a filter that doesn't expire."
+        # }
+      },
+      required: [:phrase, :context],
+      example: %{
+        "phrase" => "knights",
+        "context" => ["home"]
+      }
+    }
+  end
 end
diff --git a/lib/pleroma/web/api_spec/schemas/filter.ex b/lib/pleroma/web/api_spec/schemas/filter.ex
deleted file mode 100644
index fc5480b71..000000000
--- a/lib/pleroma/web/api_spec/schemas/filter.ex
+++ /dev/null
@@ -1,51 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.ApiSpec.Schemas.Filter do
-  alias OpenApiSpex.Schema
-  require OpenApiSpex
-
-  OpenApiSpex.schema(%{
-    title: "Filter",
-    type: :object,
-    properties: %{
-      id: %Schema{type: :string},
-      phrase: %Schema{type: :string, description: "The text to be filtered"},
-      context: %Schema{
-        type: :array,
-        items: %Schema{type: :string, enum: ["home", "notifications", "public", "thread"]},
-        description: "The contexts in which the filter should be applied."
-      },
-      expires_at: %Schema{
-        type: :string,
-        format: :"date-time",
-        description:
-          "When the filter should no longer be applied. String (ISO 8601 Datetime), or null if the filter does not expire.",
-        nullable: true
-      },
-      irreversible: %Schema{
-        type: :boolean,
-        description:
-          "Should matching entities in home and notifications be dropped by the server?"
-      },
-      whole_word: %Schema{
-        type: :boolean,
-        description: "Should the filter consider word boundaries?"
-      }
-    },
-    example: %{
-      "id" => "5580",
-      "phrase" => "@twitter.com",
-      "context" => [
-        "home",
-        "notifications",
-        "public",
-        "thread"
-      ],
-      "whole_word" => false,
-      "expires_at" => nil,
-      "irreversible" => true
-    }
-  })
-end
diff --git a/lib/pleroma/web/api_spec/schemas/filter_create_request.ex b/lib/pleroma/web/api_spec/schemas/filter_create_request.ex
deleted file mode 100644
index f2a475b12..000000000
--- a/lib/pleroma/web/api_spec/schemas/filter_create_request.ex
+++ /dev/null
@@ -1,30 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.ApiSpec.Schemas.FilterCreateRequest do
-  alias OpenApiSpex.Schema
-  require OpenApiSpex
-
-  OpenApiSpex.schema(%{
-    title: "FilterCreateRequest",
-    allOf: [
-      %OpenApiSpex.Reference{"$ref": "#/components/schemas/FilterUpdateRequest"},
-      %Schema{
-        type: :object,
-        properties: %{
-          irreversible: %Schema{
-            type: :bolean,
-            description:
-              "Should the server irreversibly drop matching entities from home and notifications?",
-            default: false
-          }
-        }
-      }
-    ],
-    example: %{
-      "phrase" => "knights",
-      "context" => ["home"]
-    }
-  })
-end
diff --git a/lib/pleroma/web/api_spec/schemas/filter_update_request.ex b/lib/pleroma/web/api_spec/schemas/filter_update_request.ex
deleted file mode 100644
index e703db0ce..000000000
--- a/lib/pleroma/web/api_spec/schemas/filter_update_request.ex
+++ /dev/null
@@ -1,41 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.ApiSpec.Schemas.FilterUpdateRequest do
-  alias OpenApiSpex.Schema
-  require OpenApiSpex
-
-  OpenApiSpex.schema(%{
-    title: "FilterUpdateRequest",
-    type: :object,
-    properties: %{
-      phrase: %Schema{type: :string, description: "The text to be filtered"},
-      context: %Schema{
-        type: :array,
-        items: %Schema{type: :string, enum: ["home", "notifications", "public", "thread"]},
-        description:
-          "Array of enumerable strings `home`, `notifications`, `public`, `thread`. At least one context must be specified."
-      },
-      irreversible: %Schema{
-        type: :bolean,
-        description:
-          "Should the server irreversibly drop matching entities from home and notifications?"
-      },
-      whole_word: %Schema{type: :bolean, description: "Consider word boundaries?", default: true}
-      # TODO: probably should implement filter expiration
-      # expires_in: %Schema{
-      #   type: :string,
-      #   format: :"date-time",
-      #   description:
-      #     "ISO 8601 Datetime for when the filter expires. Otherwise,
-      #  null for a filter that doesn't expire."
-      # }
-    },
-    required: [:phrase, :context],
-    example: %{
-      "phrase" => "knights",
-      "context" => ["home"]
-    }
-  })
-end
diff --git a/lib/pleroma/web/api_spec/schemas/filters_response.ex b/lib/pleroma/web/api_spec/schemas/filters_response.ex
deleted file mode 100644
index 8c56c5982..000000000
--- a/lib/pleroma/web/api_spec/schemas/filters_response.ex
+++ /dev/null
@@ -1,40 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.ApiSpec.Schemas.FiltersResponse do
-  require OpenApiSpex
-  alias Pleroma.Web.ApiSpec.Schemas.Filter
-
-  OpenApiSpex.schema(%{
-    title: "FiltersResponse",
-    description: "Array of Filters",
-    type: :array,
-    items: Filter,
-    example: [
-      %{
-        "id" => "5580",
-        "phrase" => "@twitter.com",
-        "context" => [
-          "home",
-          "notifications",
-          "public",
-          "thread"
-        ],
-        "whole_word" => false,
-        "expires_at" => nil,
-        "irreversible" => true
-      },
-      %{
-        "id" => "6191",
-        "phrase" => ":eurovision2019:",
-        "context" => [
-          "home"
-        ],
-        "whole_word" => true,
-        "expires_at" => "2019-05-21T13:47:31.333Z",
-        "irreversible" => false
-      }
-    ]
-  })
-end
diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
index dd13a8a09..21dc374cd 100644
--- a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
@@ -35,7 +35,7 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
       context: params.context,
       hide: params.irreversible,
       whole_word: params.whole_word
-      # expires_at
+      # TODO: support `expires_in` parameter (as in Mastodon API)
     }
 
     {:ok, response} = Filter.create(query)
@@ -57,13 +57,12 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
       ) do
     params =
       params
-      |> Map.from_struct()
       |> Map.delete(:irreversible)
-      |> Map.put(:hide, params.irreversible)
+      |> Map.put(:hide, params[:irreversible])
       |> Enum.reject(fn {_key, value} -> is_nil(value) end)
       |> Map.new()
 
-    # TODO: add expires_in -> expires_at
+    # TODO: support `expires_in` parameter (as in Mastodon API)
 
     with %Filter{} = filter <- Filter.get(filter_id, user),
          {:ok, %Filter{} = filter} <- Filter.update(filter, params) do
diff --git a/lib/pleroma/web/mastodon_api/views/filter_view.ex b/lib/pleroma/web/mastodon_api/views/filter_view.ex
index 8d5c381ec..aeff646f5 100644
--- a/lib/pleroma/web/mastodon_api/views/filter_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/filter_view.ex
@@ -7,8 +7,8 @@ defmodule Pleroma.Web.MastodonAPI.FilterView do
   alias Pleroma.Web.CommonAPI.Utils
   alias Pleroma.Web.MastodonAPI.FilterView
 
-  def render("index.json", %{filters: filters} = opts) do
-    render_many(filters, FilterView, "show.json", opts)
+  def render("index.json", %{filters: filters}) do
+    render_many(filters, FilterView, "show.json")
   end
 
   def render("show.json", %{filter: filter}) do
diff --git a/test/web/mastodon_api/controllers/filter_controller_test.exs b/test/web/mastodon_api/controllers/filter_controller_test.exs
index 41a290eb2..f29547d13 100644
--- a/test/web/mastodon_api/controllers/filter_controller_test.exs
+++ b/test/web/mastodon_api/controllers/filter_controller_test.exs
@@ -5,15 +5,8 @@
 defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
   use Pleroma.Web.ConnCase
 
-  alias Pleroma.Web.ApiSpec
-  alias Pleroma.Web.ApiSpec.Schemas.Filter
-  alias Pleroma.Web.ApiSpec.Schemas.FilterCreateRequest
-  alias Pleroma.Web.ApiSpec.Schemas.FiltersResponse
-  alias Pleroma.Web.ApiSpec.Schemas.FilterUpdateRequest
   alias Pleroma.Web.MastodonAPI.FilterView
 
-  import OpenApiSpex.TestAssertions
-
   test "creating a filter" do
     %{conn: conn} = oauth_access(["write:filters"])
 
@@ -27,13 +20,12 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
       |> put_req_header("content-type", "application/json")
       |> post("/api/v1/filters", %{"phrase" => filter.phrase, context: filter.context})
 
-    assert response = json_response(conn, 200)
+    assert response = json_response_and_validate_schema(conn, 200)
     assert response["phrase"] == filter.phrase
     assert response["context"] == filter.context
     assert response["irreversible"] == false
     assert response["id"] != nil
     assert response["id"] != ""
-    assert_schema(response, "Filter", ApiSpec.spec())
   end
 
   test "fetching a list of filters" do
@@ -59,7 +51,7 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
     response =
       conn
       |> get("/api/v1/filters")
-      |> json_response(200)
+      |> json_response_and_validate_schema(200)
 
     assert response ==
              render_json(
@@ -67,8 +59,6 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
                "index.json",
                filters: [filter_two, filter_one]
              )
-
-    assert_schema(response, "FiltersResponse", ApiSpec.spec())
   end
 
   test "get a filter" do
@@ -85,8 +75,7 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
 
     conn = get(conn, "/api/v1/filters/#{filter.filter_id}")
 
-    assert response = json_response(conn, 200)
-    assert_schema(response, "Filter", ApiSpec.spec())
+    assert response = json_response_and_validate_schema(conn, 200)
   end
 
   test "update a filter" do
@@ -115,11 +104,10 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
         context: new.context
       })
 
-    assert response = json_response(conn, 200)
+    assert response = json_response_and_validate_schema(conn, 200)
     assert response["phrase"] == new.phrase
     assert response["context"] == new.context
     assert response["irreversible"] == true
-    assert_schema(response, "Filter", ApiSpec.spec())
   end
 
   test "delete a filter" do
@@ -136,33 +124,6 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
 
     conn = delete(conn, "/api/v1/filters/#{filter.filter_id}")
 
-    assert response = json_response(conn, 200)
-    assert response == %{}
-  end
-
-  describe "OpenAPI" do
-    test "Filter example matches schema" do
-      api_spec = ApiSpec.spec()
-      schema = Filter.schema()
-      assert_schema(schema.example, "Filter", api_spec)
-    end
-
-    test "FiltersResponse example matches schema" do
-      api_spec = ApiSpec.spec()
-      schema = FiltersResponse.schema()
-      assert_schema(schema.example, "FiltersResponse", api_spec)
-    end
-
-    test "FilterCreateRequest example matches schema" do
-      api_spec = ApiSpec.spec()
-      schema = FilterCreateRequest.schema()
-      assert_schema(schema.example, "FilterCreateRequest", api_spec)
-    end
-
-    test "FilterUpdateRequest example matches schema" do
-      api_spec = ApiSpec.spec()
-      schema = FilterUpdateRequest.schema()
-      assert_schema(schema.example, "FilterUpdateRequest", api_spec)
-    end
+    assert json_response_and_validate_schema(conn, 200) == %{}
   end
 end

From 32ca9f2c59369c15905f665bee3c759ae963ff91 Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Tue, 28 Apr 2020 16:25:13 +0400
Subject: [PATCH 3/3] Render mastodon-like errors in FilterController

---
 lib/pleroma/web/mastodon_api/controllers/filter_controller.ex | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
index 21dc374cd..abbf0ce02 100644
--- a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
@@ -17,7 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
     OAuthScopesPlug,
     %{scopes: ["write:filters"]} when action not in @oauth_read_actions
   )
-  
+
   defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.FilterOperation
 
   @doc "GET /api/v1/filters"