Skip to content

Commit

Permalink
Feature: Swagger for Photo API (#118)
Browse files Browse the repository at this point in the history
* Feature: Swagger for Photo API

resolves #97

- add phoenix_swagger
- serve swagger via /fh/api/swagger

* make api accessible from outside

- also resolve todos/fixmes
  • Loading branch information
fschoenfeldt authored Jul 2, 2024
1 parent 28ebd61 commit 0aaded6
Show file tree
Hide file tree
Showing 15 changed files with 264 additions and 8 deletions.
4 changes: 3 additions & 1 deletion .check.exs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@
## ...or reconfigured (e.g. disable parallel execution of ex_unit in umbrella)
# {:ex_unit, umbrella: [parallel: false]},
{:gettext, "mix gettext.extract --check-up-to-date",
fix: "mix gettext.extract --merge priv/gettext"}
fix: "mix gettext.extract --merge priv/gettext"},

## custom new tools may be added (Mix tasks or arbitrary commands)
# run phx_schema before any other tool
{:phx_swagger_generate, "mix phx.swagger.generate", order: -1}
# {:my_task, "mix my_task", env: %{"MIX_ENV" => "prod"}},
# {:my_tool, ["my_tool", "arg with spaces"]}
]
Expand Down
2 changes: 2 additions & 0 deletions .credo.exs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
# In the latter case `**/*.{ex,exs}` will be used.
#
included: [
"config/",
"lib/",
"src/",
"test/",
Expand Down Expand Up @@ -168,6 +169,7 @@
{Credo.Check.Refactor.ModuleDependencies,
[
excluded_namespaces: [
"Fotohaecker.Application",
"Fotohaecker.Content",
"FotohaeckerWeb",
"FotohaeckerWeb.Endpoint",
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ fotohaecker-*.tar
/priv/static/assets/
/priv/static/uploads/*

# Ignore swagger files
/priv/static/schema.json

# Ignore digested assets cache.
/priv/static/cache_manifest.json

Expand Down
14 changes: 14 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,20 @@ config :ueberauth, Ueberauth.Strategy.Auth0.OAuth,
client_id: System.get_env("AUTH0_CLIENT_ID"),
client_secret: System.get_env("AUTH0_CLIENT_SECRET")

# Configure Swagger
config :fotohaecker, :phoenix_swagger,
swagger_files: %{
"priv/static/schema.json" => [
# phoenix routes will be converted to swagger paths
router: FotohaeckerWeb.Router,
# (optional) endpoint config used to set host, port and https schemes.
endpoint: FotohaeckerWeb.Endpoint
]
}

config :phoenix_swagger,
json_library: Jason

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs"
2 changes: 2 additions & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ config :fotohaecker, FotohaeckerWeb.Endpoint,
# Binding to loopback ipv4 address prevents access from other machines.
# Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
http: [ip: {0, 0, 0, 0}, port: 1337],
# TODO: test this url doesn't break anything
url: [host: "localhost", port: 1337],
static_url: [path: "/fh"],
check_origin: false,
code_reloader: true,
Expand Down
3 changes: 2 additions & 1 deletion config/prod.exs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import Config
# which you should run after static files are built and
# before starting your production server.
config :fotohaecker, FotohaeckerWeb.Endpoint,
cache_static_manifest: "priv/static/cache_manifest.json"
cache_static_manifest: "priv/static/cache_manifest.json",
url: [host: System.get_env("PHX_HOST"), port: 443]

# Do not print debug messages in production
config :logger, level: :info
Expand Down
11 changes: 11 additions & 0 deletions lib/fotohaecker/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ defmodule Fotohaecker.Application do
}
]

# Parse the swagger schema on startup as described here:
# - https://hexdocs.pm/phoenix_swagger/PhoenixSwagger.Validator.html#parse_swagger_schema/1
# - https://github.com/xerions/phoenix_swagger/issues/62#issuecomment-381932391
[
:code.priv_dir(:fotohaecker),
"static",
"schema.json"
]
|> Path.join()
|> PhoenixSwagger.Validator.parse_swagger_schema()

# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Fotohaecker.Supervisor]
Expand Down
15 changes: 15 additions & 0 deletions lib/fotohaecker_web/controllers/error_json.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
defmodule FotohaeckerWeb.ErrorJSON do
@behaviour FotohaeckerWeb.SchemaBehaviour

def schema do
use PhoenixSwagger

swagger_schema do
title("Error")
description("Error")

properties do
errors(array(:object))
end
end
end

def not_found(_conn) do
%{
errors: [
Expand Down
63 changes: 62 additions & 1 deletion lib/fotohaecker_web/controllers/photo_controller.ex
Original file line number Diff line number Diff line change
@@ -1,23 +1,64 @@
defmodule FotohaeckerWeb.PhotoController do
use FotohaeckerWeb, :controller
use PhoenixSwagger

alias Fotohaecker.Content
alias Fotohaecker.Content.Photo

action_fallback FotohaeckerWeb.FallbackController

# TODO: result pagination
def swagger_definitions do
%{
Photo: FotohaeckerWeb.PhotoJSON.schema()
}
end

swagger_path :index do
tag("Photo")
description("List all photos")
response(200, "OK", Schema.ref(:Photo))
end

# TODO: add pagination
# see https://github.com/fschoenfeldt/fotohaecker/issues/119

def index(conn, _params) do
photos = Content.list_photos()
render(conn, :index, photos: photos)
end

swagger_path :show do
tag("Photo")
description("Get a photo by ID")

parameters do
id(:path, :integer, "Photo ID", required: true)
end

response(200, "OK", Schema.ref(:Photo))
end

def show(conn, %{"id" => id}) do
with %Photo{} = photo <- Content.get_photo(id) do
render(conn, :show, photo: photo)
end
end

swagger_path :search do
tag("Photo")
description("Search for photos")
deprecated(true)

parameters do
search(:path, :string, "Search string", required: true)
end

response(200, "OK", Schema.ref(:Photo))
end

# TODO: add pagination
# see https://github.com/fschoenfeldt/fotohaecker/issues/119

def search(conn, %{"search" => search}) do
# decode uri encoded search string
search = URI.decode(search)
Expand All @@ -26,6 +67,26 @@ defmodule FotohaeckerWeb.PhotoController do
render(conn, :index, photos: photos)
end

swagger_path :search_query do
tag("Photo")
description("Search for photos")
deprecated(true)

parameters do
query(:query, :string, "Search string", required: true)
end

response(200, "OK", Schema.ref(:Photo))
end

def search_query(conn, %{"query" => search}) do
# decode uri encoded search string
search = URI.decode(search)

photos = Fotohaecker.Search.search!(search)
render(conn, :index, photos: photos)
end

# def create(conn, %{"photo" => photo_params}) do
# with {:ok, %Photo{} = photo} <- Contents.create_photo(photo_params) do
# conn
Expand Down
45 changes: 44 additions & 1 deletion lib/fotohaecker_web/controllers/photo_json.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule FotohaeckerWeb.PhotoJSON do
alias Fotohaecker.Search
@behaviour FotohaeckerWeb.SchemaBehaviour

alias Fotohaecker.Content.Photo
alias Fotohaecker.Search

@doc """
Renders a list of photos.
Expand All @@ -18,6 +20,47 @@ defmodule FotohaeckerWeb.PhotoJSON do
}
end

@doc """
Schema for usage in swagger
# TODO: enforce schema function by using elixirs behaviour
"""
def schema do
use PhoenixSwagger

swagger_schema do
title("Photo")
description("A photo")

properties do
id(:integer, "Photo ID", required: true)
title(:string, "Photo title", required: true)
tags(array(:string), "Photo tags")

links(
Schema.new do
property(:html, :string, "HTML link to photo")
end
)

urls(
Schema.new do
property(:thumb1x, :string, "Small Thumbnail URL")
property(:thumb2x, :string, "Medium Thumb URL")
property(:thumb3x, :string, "Large Thumb URL")
property(:raw, :string, "Original photo URL")
property(:full, :string, "Full size photo URL")
end
)

description(:string, "Photo description")
url(:string, "Photo URL", required: true)
inserted_at(:string, "Creation timestamp", format: "date-time")
updated_at(:string, "Last update timestamp", format: "date-time")
end
end
end

# Instead of deriving the Jason.Encoder protocol for the Photo struct,
# we can also define a data/1 function that returns a map with the
# desired fields.
Expand Down
18 changes: 18 additions & 0 deletions lib/fotohaecker_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ defmodule FotohaeckerWeb.Router do

pipeline :api do
plug CORSPlug
plug PhoenixSwagger.Plug.Validate

plug :accepts, ["json"]
end
Expand All @@ -21,6 +22,12 @@ defmodule FotohaeckerWeb.Router do
end

scope "/fh" do
scope "/api/swagger", PhoenixSwagger do
forward "/", Plug.SwaggerUI,
otp_app: :fotohaecker,
swagger_file: "schema.json"
end

scope "/api", FotohaeckerWeb do
pipe_through :api

Expand All @@ -32,6 +39,7 @@ defmodule FotohaeckerWeb.Router do
scope "/search" do
scope "/photos" do
get "/:search", PhotoController, :search
get "/", PhotoController, :search_query
end
end
end
Expand Down Expand Up @@ -98,4 +106,14 @@ defmodule FotohaeckerWeb.Router do
forward "/mailbox", Plug.Swoosh.MailboxPreview
end
end

def swagger_info do
%{
# basePath: "/fh/api",
info: %{
version: "1.0",
title: "Fotohaecker"
}
}
end
end
7 changes: 7 additions & 0 deletions lib/fotohaecker_web/schema_behaviour.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule FotohaeckerWeb.SchemaBehaviour do
@moduledoc """
Behaviour for schema modules that are used to generate Swagger schemas.
"""

@callback schema() :: PhoenixSwagger.Schema
end
7 changes: 5 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ defmodule Fotohaecker.MixProject do
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps(),
dialyzer: [plt_add_apps: [:mix]]
dialyzer: [plt_add_apps: [:mix]],
compilers: [:phoenix_swagger] ++ Mix.compilers()
]
end

Expand Down Expand Up @@ -66,7 +67,9 @@ defmodule Fotohaecker.MixProject do
{:heroicons, "~> 0.5.0"},
{:mox, "~> 1.0", only: :test},
{:stripity_stripe, "~> 2.0"},
{:cors_plug, "~> 3.0"}
{:cors_plug, "~> 3.0"},
{:phoenix_swagger, "~> 0.8"},
{:ex_json_schema, "~> 0.5"}
]
end

Expand Down
2 changes: 2 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"},
"ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"},
"ex_doc": {:hex, :ex_doc, "0.32.2", "f60bbeb6ccbe75d005763e2a328e6f05e0624232f2393bc693611c2d3ae9fa0e", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "a4480305cdfe7fdfcbb77d1092c76161626d9a7aa4fb698aee745996e34602df"},
"ex_json_schema": {:hex, :ex_json_schema, "0.7.4", "09eb5b0c8184e5702bc89625a9d0c05c7a0a845d382e9f6f406a0fc1c9a8cc3f", [:mix], [], "hexpm", "45c67fa840f0d719a2b5578126dc29bcdc1f92499c0f61bcb8a3bcb5935f9684"},
"excellent_migrations": {:hex, :excellent_migrations, "0.1.8", "2cffa1c795f7501559fffb6bf090f4205dee02f129ba4535a0465c2db7dc310b", [:mix], [{:credo, "~> 1.5", [hex: :credo, repo: "hexpm", optional: true]}], "hexpm", "9b61cf287a8e50c5f4a7950bc684c4a2af05d40a14ffe46bd8ac522d084e5840"},
"expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"},
"exqlite": {:hex, :exqlite, "0.22.0", "8bc24a2b807f34ae1af15203f16668bf6abd171b35e4097d7ad56a717f9bafa8", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "3ea23b9fab54d68815281cac15ca4c7c4cbd0e8832dffe4bd395742f938e87c0"},
Expand Down Expand Up @@ -51,6 +52,7 @@
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.20.14", "70fa101aa0539e81bed4238777498f6215e9dda3461bdaa067cad6908110c364", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, 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.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "82f6d006c5264f979ed5eb75593d808bbe39020f20df2e78426f4f2d570e2402"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
"phoenix_swagger": {:hex, :phoenix_swagger, "0.8.3", "298d6204802409d3b0b4fc1013873839478707cf3a62532a9e10fec0e26d0e37", [:mix], [{:ex_json_schema, "~> 0.7.1", [hex: :ex_json_schema, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.11", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "3bc0fa9f5b679b8a61b90a52b2c67dd932320e9a84a6f91a4af872a0ab367337"},
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"phoenix_view": {:hex, :phoenix_view, "2.0.3", "4d32c4817fce933693741deeb99ef1392619f942633dde834a5163124813aad3", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "cd34049af41be2c627df99cd4eaa71fc52a328c0c3d8e7d4aa28f880c30e7f64"},
"plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"},
Expand Down
Loading

0 comments on commit 0aaded6

Please sign in to comment.