From d6c9826e70ff0b5d825856df57baf5008d334ef9 Mon Sep 17 00:00:00 2001 From: Tom Konidas Date: Thu, 5 Sep 2024 21:53:25 -0400 Subject: [PATCH] Add Apps GET API documentation --- .formatter.exs | 2 +- config/dev.exs | 2 + lib/plexus_web/api_spec.ex | 17 +++++++ .../controllers/api/v1/app_controller.ex | 38 +++++++++++++++ .../controllers/api/v1/schemas/app.ex | 37 +++++++++++++++ .../api/v1/schemas/app_response.ex | 37 +++++++++++++++ .../api/v1/schemas/apps_response.ex | 46 +++++++++++++++++++ .../controllers/api/v1/schemas/page.ex | 22 +++++++++ .../controllers/api/v1/schemas/score.ex | 22 +++++++++ .../controllers/api/v1/schemas/scores.ex | 29 ++++++++++++ lib/plexus_web/router.ex | 11 ++++- mix.exs | 6 ++- mix.lock | 2 + 13 files changed, 266 insertions(+), 5 deletions(-) create mode 100644 lib/plexus_web/api_spec.ex create mode 100644 lib/plexus_web/controllers/api/v1/schemas/app.ex create mode 100644 lib/plexus_web/controllers/api/v1/schemas/app_response.ex create mode 100644 lib/plexus_web/controllers/api/v1/schemas/apps_response.ex create mode 100644 lib/plexus_web/controllers/api/v1/schemas/page.ex create mode 100644 lib/plexus_web/controllers/api/v1/schemas/score.ex create mode 100644 lib/plexus_web/controllers/api/v1/schemas/scores.ex diff --git a/.formatter.exs b/.formatter.exs index ef8840ce..9e81bfe8 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,5 +1,5 @@ [ - import_deps: [:ecto, :ecto_sql, :phoenix], + import_deps: [:open_api_spex, :ecto, :ecto_sql, :phoenix], subdirectories: ["priv/*/migrations"], plugins: [Phoenix.LiveView.HTMLFormatter], inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"] diff --git a/config/dev.exs b/config/dev.exs index 23e09eea..744897f8 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -10,6 +10,8 @@ config :plexus, Plexus.Repo, show_sensitive_data_on_connection_error: true, pool_size: 10 +config :open_api_spex, :cache_adapter, OpenApiSpex.Plug.NoneCache + # For development, we disable any cache and enable # debugging and code reloading. # diff --git a/lib/plexus_web/api_spec.ex b/lib/plexus_web/api_spec.ex new file mode 100644 index 00000000..078355a2 --- /dev/null +++ b/lib/plexus_web/api_spec.ex @@ -0,0 +1,17 @@ +defmodule PlexusWeb.ApiSpec do + @behaviour OpenApiSpex.OpenApi + + @impl OpenApiSpex.OpenApi + def spec do + OpenApiSpex.resolve_schema_modules(%OpenApiSpex.OpenApi{ + servers: [ + OpenApiSpex.Server.from_endpoint(PlexusWeb.Endpoint) + ], + info: %OpenApiSpex.Info{ + title: "Plexus", + version: to_string(Application.spec(:plexus, :vsn)) + }, + paths: OpenApiSpex.Paths.from_router(PlexusWeb.Router) + }) + end +end diff --git a/lib/plexus_web/controllers/api/v1/app_controller.ex b/lib/plexus_web/controllers/api/v1/app_controller.ex index cd47e4aa..b960d3c8 100644 --- a/lib/plexus_web/controllers/api/v1/app_controller.ex +++ b/lib/plexus_web/controllers/api/v1/app_controller.ex @@ -1,17 +1,36 @@ defmodule PlexusWeb.API.V1.AppController do use PlexusWeb, :controller + use OpenApiSpex.ControllerSpecs alias Plexus.Apps + alias PlexusWeb.API.V1.Schemas.AppResponse + alias PlexusWeb.API.V1.Schemas.AppsResponse alias PlexusWeb.Params action_fallback PlexusWeb.FallbackController + tags ["apps"] + + operation :index, + summary: "List Applications", + parameters: [ + page: [in: :query, description: "Page number", type: :integer, example: 2], + limit: [in: :query, description: "Max results per page", type: :integer, example: 25], + scores: [in: :query, description: "Include scores", type: :boolean, example: true], + q: [in: :query, description: "Search query", type: :string, example: "YouTube"] + ], + responses: [ + ok: {"Applications", "application/json", AppsResponse} + ] + def index(conn, params) do opts = build_opts(params) page = Apps.list_apps(opts) render(conn, :index, page: page) end + operation :create, false + def create(conn, %{"app" => params}) do schema = %{ package: {:string, required: true}, @@ -28,6 +47,22 @@ defmodule PlexusWeb.API.V1.AppController do end end + operation :show, + summary: "Get Application", + parameters: [ + package: [ + in: :path, + description: "Android Package", + type: :string, + required: true, + example: "com.google.android.youtube" + ], + scores: [in: :query, description: "Include scores", type: :boolean, example: true] + ], + responses: [ + ok: {"Applications", "application/json", AppResponse} + ] + def show(conn, %{"package" => package} = params) do opts = build_opts(params) app = Apps.get_app!(package, opts) @@ -36,6 +71,9 @@ defmodule PlexusWeb.API.V1.AppController do defp build_opts(params) do Enum.reduce(params, [], fn + {"q", search_term}, acc -> + Keyword.put(acc, :search_term, search_term) + {"scores", "true"}, acc -> Keyword.put(acc, :scores, true) diff --git a/lib/plexus_web/controllers/api/v1/schemas/app.ex b/lib/plexus_web/controllers/api/v1/schemas/app.ex new file mode 100644 index 00000000..8f8a9411 --- /dev/null +++ b/lib/plexus_web/controllers/api/v1/schemas/app.ex @@ -0,0 +1,37 @@ +defmodule PlexusWeb.API.V1.Schemas.App do + alias OpenApiSpex.Schema + alias PlexusWeb.API.V1.Schemas.Scores + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "App", + description: "A Representation of an App", + type: :object, + properties: %{ + name: %Schema{type: :string, description: "Name"}, + package: %Schema{type: :string, description: "Android Package"}, + icon_url: %Schema{type: :string, description: "URL of Icon"}, + scores: Scores + }, + example: %{ + "name" => "YouTube Music", + "package" => "com.google.android.youtube.tvmusic", + "scores" => %{ + "native" => %{ + "rating_type" => "native", + "numerator" => 1.2, + "total_count" => 21, + "denominator" => 4 + }, + "micro_g" => %{ + "rating_type" => "micro_g", + "numerator" => 4.0, + "total_count" => 44, + "denominator" => 4 + } + }, + "icon_url" => + "https://play-lh.googleusercontent.com/76AjYITcB0dI0sFqdQjNgXQxRMlDIswbp0BAU_O5Oob-73b6cqKggVlAiNXQAW5Bl1g" + } + }) +end diff --git a/lib/plexus_web/controllers/api/v1/schemas/app_response.ex b/lib/plexus_web/controllers/api/v1/schemas/app_response.ex new file mode 100644 index 00000000..5a4eb371 --- /dev/null +++ b/lib/plexus_web/controllers/api/v1/schemas/app_response.ex @@ -0,0 +1,37 @@ +defmodule PlexusWeb.API.V1.Schemas.AppResponse do + alias PlexusWeb.API.V1.Schemas.App + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "AppResponse", + description: "Response schema for an application", + type: :object, + properties: %{ + data: App + }, + example: %{ + "data" => [ + %{ + "name" => "YouTube Music", + "package" => "com.google.android.youtube.tvmusic", + "scores" => %{ + "native" => %{ + "rating_type" => "native", + "numerator" => 1.2, + "total_count" => 21, + "denominator" => 4 + }, + "micro_g" => %{ + "rating_type" => "micro_g", + "numerator" => 3.9, + "total_count" => 44, + "denominator" => 4 + } + }, + "icon_url" => + "https://play-lh.googleusercontent.com/lMoItBgdPPVDJsNOVtP26EKHePkwBg-PkuY9NOrc-fumRtTFP4XhpUNk_22syN4Datc" + } + ] + } + }) +end diff --git a/lib/plexus_web/controllers/api/v1/schemas/apps_response.ex b/lib/plexus_web/controllers/api/v1/schemas/apps_response.ex new file mode 100644 index 00000000..7e027de7 --- /dev/null +++ b/lib/plexus_web/controllers/api/v1/schemas/apps_response.ex @@ -0,0 +1,46 @@ +defmodule PlexusWeb.API.V1.Schemas.AppsResponse do + alias OpenApiSpex.Schema + alias PlexusWeb.API.V1.Schemas.App + alias PlexusWeb.API.V1.Schemas.Page + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "AppsResponse", + description: "Response schema for a list of applications", + type: :object, + properties: %{ + data: %Schema{type: :array, items: App}, + meta: Page + }, + example: %{ + "data" => [ + %{ + "name" => "YouTube Music", + "package" => "com.google.android.youtube.tvmusic", + "scores" => %{ + "native" => %{ + "rating_type" => "native", + "numerator" => 1.1, + "total_count" => 21, + "denominator" => 4 + }, + "micro_g" => %{ + "rating_type" => "micro_g", + "numerator" => 3.7, + "total_count" => 43, + "denominator" => 4 + } + }, + "icon_url" => + "https://play-lh.googleusercontent.com/76AjYITcB0dI0sFqdQjNgXQxRMlDIswbp0BAU_O5Oob-73b6cqKggVlAiNXQAW5Bl1g" + } + ], + "meta" => %{ + "page_number" => 3, + "limit" => 1, + "total_count" => 420, + "total_pages" => 69 + } + } + }) +end diff --git a/lib/plexus_web/controllers/api/v1/schemas/page.ex b/lib/plexus_web/controllers/api/v1/schemas/page.ex new file mode 100644 index 00000000..5d632af8 --- /dev/null +++ b/lib/plexus_web/controllers/api/v1/schemas/page.ex @@ -0,0 +1,22 @@ +defmodule PlexusWeb.API.V1.Schemas.Page do + alias OpenApiSpex.Schema + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "Page", + description: "Page metadata", + type: :object, + properties: %{ + page_number: %Schema{type: :integer, description: "Current page number", default: 1}, + limit: %Schema{type: :integer, description: "Max results per page", default: 25}, + total_count: %Schema{type: :integer, description: "Total count of results", readOnly: true}, + total_pages: %Schema{type: :integer, description: "Total count of pages", readOnly: true} + }, + example: %{ + "page_number" => 3, + "limit" => 25, + "total_count" => 420, + "total_pages" => 69 + } + }) +end diff --git a/lib/plexus_web/controllers/api/v1/schemas/score.ex b/lib/plexus_web/controllers/api/v1/schemas/score.ex new file mode 100644 index 00000000..e9211f0a --- /dev/null +++ b/lib/plexus_web/controllers/api/v1/schemas/score.ex @@ -0,0 +1,22 @@ +defmodule PlexusWeb.API.V1.Schemas.Score do + alias OpenApiSpex.Schema + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "Score", + description: "A Representation of a Score", + type: :object, + properties: %{ + rating_type: %Schema{type: :string, description: "Rating Type", enum: ["micro_g", "native"]}, + numenator: %Schema{type: :number, description: "Numenator"}, + denominator: %Schema{type: :integer, description: "Denominator"}, + total_count: %Schema{type: :string, description: "Total count of ratings"} + }, + example: %{ + "numerator" => 4.0, + "denominator" => 4, + "rating_type" => "micro_g", + "total_count" => 69 + } + }) +end diff --git a/lib/plexus_web/controllers/api/v1/schemas/scores.ex b/lib/plexus_web/controllers/api/v1/schemas/scores.ex new file mode 100644 index 00000000..1e0bb0d2 --- /dev/null +++ b/lib/plexus_web/controllers/api/v1/schemas/scores.ex @@ -0,0 +1,29 @@ +defmodule PlexusWeb.API.V1.Schemas.Scores do + alias PlexusWeb.API.V1.Schemas.Score + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "Scores", + description: "A Representation of Scores", + type: :object, + properties: %{ + micro_g: Score, + native: Score + }, + derive?: false, + example: %{ + "micro_g" => %{ + "numerator" => 4.0, + "denominator" => 4, + "rating_type" => "micro_g", + "total_count" => 69 + }, + "native" => %{ + "numerator" => 3.7, + "denominator" => 4, + "rating_type" => "native", + "total_count" => 69 + } + } + }) +end diff --git a/lib/plexus_web/router.ex b/lib/plexus_web/router.ex index 5292cc7c..58edddf4 100644 --- a/lib/plexus_web/router.ex +++ b/lib/plexus_web/router.ex @@ -12,6 +12,7 @@ defmodule PlexusWeb.Router do pipeline :api do plug :accepts, ["json"] + plug OpenApiSpex.Plug.PutApiSpec, module: PlexusWeb.ApiSpec end pipeline :authenticated_device do @@ -22,10 +23,11 @@ defmodule PlexusWeb.Router do plug :auth end - scope "/", PlexusWeb do + scope "/" do pipe_through :browser - live "/", AppLive.Index, :index + live "/", PlexusWeb.AppLive.Index, :index + get "/swaggerui", OpenApiSpex.Plug.SwaggerUI, path: "/api/openapi" end scope "/admin", PlexusWeb.Admin do @@ -41,6 +43,11 @@ defmodule PlexusWeb.Router do live "/apps/:package/ratings/:rating_type", RatingLive.Index, :index end + scope "/api" do + pipe_through :api + get "/openapi", OpenApiSpex.Plug.RenderSpec, [] + end + scope "/api/v1", PlexusWeb.API.V1 do pipe_through :api diff --git a/mix.exs b/mix.exs index 02abe13c..935947e9 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Plexus.MixProject do def project do [ app: :plexus, - version: "0.1.0", + version: "1.0.0", elixir: "~> 1.15", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, @@ -59,7 +59,9 @@ defmodule Plexus.MixProject do {:plug_cowboy, "~> 2.6"}, {:swoosh, "~> 1.3"}, {:gen_smtp, "~> 1.2"}, - {:finch, "~> 0.13"} + {:finch, "~> 0.13"}, + {:open_api_spex, "~> 3.20"}, + {:ymlr, "~> 5.1"} ] end diff --git a/mix.lock b/mix.lock index a4cb76f6..658c0ddc 100644 --- a/mix.lock +++ b/mix.lock @@ -26,6 +26,7 @@ "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "nimble_totp": {:hex, :nimble_totp, "1.0.0", "79753bae6ce59fd7cacdb21501a1dbac249e53a51c4cd22b34fa8438ee067283", [:mix], [], "hexpm", "6ce5e4c068feecdb782e85b18237f86f66541523e6bad123e02ee1adbe48eda9"}, + "open_api_spex": {:hex, :open_api_spex, "3.20.1", "ce5b3db013cd7337ab147f39fa2d4d627ddeeb4ff3fea34792f43d7e2e654605", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "dc9c383949d0fc4b20b73103ac20af39dad638b3a15c0e6281853c2fc7cc3cc8"}, "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.2", "3b83b24ab5a2eb071a20372f740d7118767c272db386831b2e77638c4dcc606d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "3f94d025f59de86be00f5f8c5dd7b5965a3298458d21ab1c328488be3b5fcd59"}, "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, @@ -49,4 +50,5 @@ "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"}, + "ymlr": {:hex, :ymlr, "5.1.3", "a8061add5a378e20272a31905be70209a5680fdbe0ad51f40cb1af4bdd0a010b", [:mix], [], "hexpm", "8663444fa85101a117887c170204d4c5a2182567e5f84767f0071cf15f2efb1e"}, }