Add OpenAPI spec for FilterController
authorEgor Kislitsyn <egor@kislitsyn.com>
Tue, 14 Apr 2020 14:36:32 +0000 (18:36 +0400)
committerEgor Kislitsyn <egor@kislitsyn.com>
Tue, 5 May 2020 13:41:01 +0000 (17:41 +0400)
lib/pleroma/filter.ex
lib/pleroma/web/api_spec/operations/filter_operation.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/schemas/filter.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/schemas/filter_create_request.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/schemas/filter_update_request.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/schemas/filters_response.ex [new file with mode: 0644]
lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
lib/pleroma/web/mastodon_api/views/filter_view.ex
test/filter_test.exs
test/web/mastodon_api/controllers/filter_controller_test.exs

index 7cb49360f1836e193ce67626d73b2d2b79db304f..4d61b36502911679f4a8e9146af4ff761bdab726 100644 (file)
@@ -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 (file)
index 0000000..0d673f5
--- /dev/null
@@ -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 (file)
index 0000000..fc5480b
--- /dev/null
@@ -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 (file)
index 0000000..f2a475b
--- /dev/null
@@ -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 (file)
index 0000000..e703db0
--- /dev/null
@@ -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 (file)
index 0000000..8c56c59
--- /dev/null
@@ -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
index 7fd0562c98ceeee9838658063a73ecd66bddbf90..dd13a8a0956f08ac00d3b74d3b4e0f3de6fe93b3 100644 (file)
@@ -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
-    }
-
-    {:ok, response} = Filter.update(query)
-    render(conn, "filter.json", filter: response)
+    params =
+      params
+      |> Map.from_struct()
+      |> Map.delete(:irreversible)
+      |> Map.put(:hide, params.irreversible)
+      |> Enum.reject(fn {_key, value} -> is_nil(value) end)
+      |> Map.new()
+
+    # 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
index 97fd1e83f731032d937d492cb5f78b80b10a1360..8d5c381ec7a5ba04826abc92287c952713d0e182 100644 (file)
@@ -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)
index b2a8330eed3e3b9ad09559831092ee6e94a1eb4f..63a30c736d0c6aed72cb29eb54c68e580cda6d40 100644 (file)
@@ -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
index 97ab005e042021efd89bf51e1384f4712156d6f9..41a290eb226f982b7217cc49ae11a2098d55d516 100644 (file)
@@ -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