Skip to content

Commit

Permalink
add OpenApi spec and some unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dealloc committed Feb 28, 2024
1 parent fb68d90 commit edab084
Show file tree
Hide file tree
Showing 19 changed files with 413 additions and 15 deletions.
2 changes: 1 addition & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[
import_deps: [:phoenix],
import_deps: [:phoenix, :open_api_spex],
plugins: [Phoenix.LiveView.HTMLFormatter],
inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"]
]
3 changes: 3 additions & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,6 @@ config :phoenix, :plug_init_mode, :runtime

# Include HEEx debug annotations as HTML comments in rendered markup
config :phoenix_live_view, :debug_heex_annotations, true

# Ensures the OpenApi spec is refreshed during development and not cached
config :open_api_spex, :cache_adapter, OpenApiSpex.Plug.NoneCache
4 changes: 4 additions & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import Config

# We don't start up any Helldiver seasons during tests to avoid hitting the API
config :helldivers_2,
war_seasons: []

# We don't run a server during test. If one is required,
# you can enable the server option below.
config :helldivers_2, Helldivers2Web.Endpoint,
Expand Down
8 changes: 8 additions & 0 deletions lib/helldivers_2_web.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ defmodule Helldivers2Web do
import Phoenix.Controller
import Phoenix.LiveView.Router
import Helldivers2Web.Plugs.RateLimit, only: [rate_limit: 2]
import Helldivers2Web.Plugs.WarSeason, only: [check_war_id: 2]
end
end

Expand All @@ -45,6 +46,13 @@ defmodule Helldivers2Web do

import Plug.Conn

# OpenApi helpers
alias OpenApiSpex.Schema
alias OpenApiSpex.JsonErrorResponse
alias OpenApiSpex.Plug.CastAndValidate
alias Helldivers2Web.Schemas.NotFoundSchema
alias Helldivers2Web.Schemas.TooManyRequestsSchema

unquote(verified_routes())
end
end
Expand Down
22 changes: 22 additions & 0 deletions lib/helldivers_2_web/api_spec.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule Helldivers2Web.ApiSpec do
alias OpenApiSpex.{Info, OpenApi, Paths, Server}
alias Helldivers2Web.{Endpoint, Router}
@behaviour OpenApi

@impl OpenApi
def spec do
%OpenApi{
servers: [
# Populate the Server info from a phoenix endpoint
Server.from_endpoint(Endpoint)
],
info: %Info{
title: "Helldivers 2",
version: "0.0.1"
},
# Populate the paths from a phoenix router
paths: Paths.from_router(Router)
}
|> OpenApiSpex.resolve_schema_modules() # Discover request/response schemas from path specs
end
end
37 changes: 33 additions & 4 deletions lib/helldivers_2_web/controllers/api/planets_controller.ex
Original file line number Diff line number Diff line change
@@ -1,19 +1,48 @@
defmodule Helldivers2Web.Api.PlanetsController do
use Helldivers2Web, :controller
use OpenApiSpex.ControllerSpecs

plug CastAndValidate, json_render_error_v2: true
action_fallback Helldivers2Web.FallbackController

alias Helldivers2.WarSeason
alias Helldivers2Web.Schemas.PlanetSchema

def index(conn, %{"war_id" => war_id}) do
operation :index,
summary: "Get an overview of all planets",
parameters: [
war_id: [in: :path, description: "The war ID", type: :integer, example: 801]
],
responses: [
ok: PlanetSchema.responses(),
not_found: NotFoundSchema.response(),
too_many_requests: TooManyRequestsSchema.response()
]
def index(conn, %{war_id: war_id}) do
with {:ok, planets} <- WarSeason.get_planets(war_id) do
render(conn, :index, planets: planets)
end
end

def show(conn, %{"war_id" => war_id, "planet_index" => planet_index}) do
with {planet_index, _} <- Integer.parse(planet_index),
{:ok, planets} <- WarSeason.get_planet(war_id, planet_index) do
operation :show,
summary: "Get information on a specific planet",
parameters: [
war_id: [in: :path, description: "The war ID", type: :integer, example: 801],
planet_index: [
in: :path,
description: "The index of the planet",
type: :integer,
example: 0
]
],
responses: [
ok: PlanetSchema.response(),
not_found: NotFoundSchema.response(),
too_many_requests: TooManyRequestsSchema.response(),
unprocessable_entity: JsonErrorResponse.response()
]
def show(conn, %{war_id: war_id, planet_index: planet_index}) do
with {:ok, planets} <- WarSeason.get_planet(war_id, planet_index) do
render(conn, :show, planet: planets)
end
end
Expand Down
33 changes: 31 additions & 2 deletions lib/helldivers_2_web/controllers/api/war_season_controller.ex
Original file line number Diff line number Diff line change
@@ -1,14 +1,43 @@
defmodule Helldivers2Web.Api.WarSeasonController do
use Helldivers2Web, :controller
alias Helldivers2.WarSeason
use OpenApiSpex.ControllerSpecs

plug CastAndValidate, json_render_error_v2: true
action_fallback Helldivers2Web.FallbackController

alias Helldivers2.WarSeason
alias Helldivers2Web.Schemas.WarInfoSchema

operation :index,
summary: "Get an overview of all available war seasons",
responses: [
ok:
{"Warseason overview", "application/json",
%Schema{type: :array, items: %Schema{type: :string}}},
too_many_requests: TooManyRequestsSchema.response()
]

def index(conn, _) do
json(conn, Application.get_env(:helldivers_2, :war_seasons))
end

def show_info(conn, %{"war_id" => war_id}) do
operation :show_info,
summary: "Get information on a war season",
externalDocs: %OpenApiSpex.ExternalDocumentation{
description: "This is a mapped version of the official WarInfo object",
url: "https://api.live.prod.thehelldiversgame.com/api/WarSeason/801/WarInfo"
},
parameters: [
war_id: [in: :path, description: "The war ID", type: :integer, example: 801]
],
responses: [
ok: WarInfoSchema.response(),
not_found: NotFoundSchema.response(),
too_many_requests: TooManyRequestsSchema.response(),
unprocessable_entity: JsonErrorResponse.response()
]

def show_info(conn, %{war_id: war_id}) do
with {:ok, war_info} <- WarSeason.get_war_info(war_id) do
render(conn, :show, war_info: war_info)
end
Expand Down
6 changes: 3 additions & 3 deletions lib/helldivers_2_web/plugs/rate_limit.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ defmodule Helldivers2Web.Plugs.RateLimit do

conn
|> put_rate_limit_headers(limit, 0)
|> put_resp_header("Retry-After", to_string(ms_to_next_bucket / 1_000))
|> put_resp_header("retry-after", to_string(ms_to_next_bucket / 1_000))
|> put_status(:too_many_requests)
|> json(%{error: "Rate limit exceeded."})
|> halt()
end

defp put_rate_limit_headers(conn, limit, remaining) do
conn
|> put_resp_header("X-RateLimit-Limit", to_string(limit))
|> put_resp_header("X-RateLimit-Remaining", to_string(remaining))
|> put_resp_header("x-ratelimit-limit", to_string(limit))
|> put_resp_header("x-ratelimit-remaining", to_string(remaining))
end
end
33 changes: 33 additions & 0 deletions lib/helldivers_2_web/plugs/war_season.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
defmodule Helldivers2Web.Plugs.WarSeason do
import Plug.Conn

alias Helldivers2Web.FallbackController
alias Helldivers2.WarSeason

@doc """
If the request contains a `war_id` it validates that it exists.
This ensures that the rest of the application can assume any `war_id` passed exists.
"""
@spec check_war_id(Plug.Conn.t()) :: Plug.Conn.t()
def check_war_id(conn, _options \\ []) do
case Map.get(conn.params, "war_id") do
nil ->
conn
war_id ->
validate_war_id(conn, war_id)
end
end

# We have a war_id, validate it exists
defp validate_war_id(conn, war_id) do
case WarSeason.exists?(war_id) do
true ->
conn
false ->
conn
|> FallbackController.call({:error, :not_found})
|> halt()
end
end
end
14 changes: 14 additions & 0 deletions lib/helldivers_2_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ defmodule Helldivers2Web.Router do

pipeline :api do
plug :accepts, ["json"]
plug OpenApiSpex.Plug.PutApiSpec, module: Helldivers2Web.ApiSpec
plug :rate_limit, [interval_seconds: 300, max_requests: 10]
plug :check_war_id
end

pipeline :openapi do
plug :accepts, ["json"]
plug OpenApiSpex.Plug.PutApiSpec, module: Helldivers2Web.ApiSpec
end

# scope "/", Helldivers2Web do
Expand All @@ -21,6 +28,13 @@ defmodule Helldivers2Web.Router do
# get "/", PageController, :home
# end

scope "/api" do
pipe_through :openapi

get "/openapi", OpenApiSpex.Plug.RenderSpec, []
get "/swaggerui", OpenApiSpex.Plug.SwaggerUI, path: "/api/openapi"
end

# Other scopes may use custom stacks.
scope "/api", Helldivers2Web.Api do
pipe_through :api
Expand Down
24 changes: 24 additions & 0 deletions lib/helldivers_2_web/schemas/home_world_schema.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defmodule Helldivers2Web.Schemas.HomeWorldSchema do
alias Helldivers2Web.Schemas.PlanetSchema
alias OpenApiSpex.Schema
require OpenApiSpex

@doc "Generates a schema for a single homeworld schema response"
def response(), do: {"Homeworld response", "application/json", __MODULE__}

OpenApiSpex.schema(%{
description: "Homeworld information of a given faction",
type: :object,
properties: %{
race: %Schema{
type: :string,
description: "The race that the planet is the homeworld of"
},
planets: %Schema{
type: :array,
items: PlanetSchema,
description: "The planets this race considers it's homeworlds"
}
}
})
end
16 changes: 16 additions & 0 deletions lib/helldivers_2_web/schemas/not_found_schema.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
defmodule Helldivers2Web.Schemas.NotFoundSchema do
alias OpenApiSpex.Schema
require OpenApiSpex

def response(), do: {"Resource not found", "application/json", __MODULE__}

OpenApiSpex.schema(%{
description: "The resource you tried to retrieve could not be found",
type: :object,
properties: %{
errors: %Schema{type: :object, properties: %{
detail: %Schema{type: :string, description: "Description of what went wrong"}
}}
}
})
end
54 changes: 54 additions & 0 deletions lib/helldivers_2_web/schemas/planet_schema.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
defmodule Helldivers2Web.Schemas.PlanetSchema do
alias OpenApiSpex.Schema
require OpenApiSpex

@doc "Generates a schema for a single planet schema response"
def response(), do: {"Planet response", "application/json", __MODULE__}

@doc "Generates a schema for an array of planet schemas"
def responses(), do: {"Planets response", "application/json", %Schema{type: :array, items: __MODULE__}}

OpenApiSpex.schema(%{
description: "Represents a planet in the galactic war that must receive Managed democracy",
type: :object,
properties: %{
index: %Schema{
type: :integer,
description:
"The index of this planet, for convenience kept the same as in the official API"
},
name: %Schema{
type: :string,
description: "The human readable name of the planet, or unknown if it's not a known name"
},
hash: %Schema{
type: :integer,
description: "A hash retrieved from the official API, purpose unknown"
},
position: %Schema{
type: :object,
description: "The coordinates in the galaxy where this planet is located",
properties: %{
x: %Schema{type: :number, description: "X coordinate"},
y: %Schema{type: :number, description: "Y coordinate"}
}
},
waypoints: %Schema{
type: :array,
description: "Waypoints, seems to link planets together but purpose unclear"
},
max_health: %Schema{
type: :integer,
description: "The maximum health of this planet, used in conflict stats"
},
disabled: %Schema{
type: :boolean,
description: "Whether or not this planet is currently playable (enabled)"
},
initial_owner: %Schema{
type: :string,
description: "Which faction originally claimed this planet"
}
}
})
end
14 changes: 14 additions & 0 deletions lib/helldivers_2_web/schemas/too_many_requests_schema.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
defmodule Helldivers2Web.Schemas.TooManyRequestsSchema do
alias OpenApiSpex.Schema
require OpenApiSpex

def response(), do: {"Rate limit exceeded", "application/json", __MODULE__}

OpenApiSpex.schema(%{
description: "You're making too many requests in a limited span of time",
type: :object,
properties: %{
error: %Schema{type: :string, description: "Error message for rate limit being exceeded"}
}
})
end
Loading

0 comments on commit edab084

Please sign in to comment.