diff --git a/.check.exs b/.check.exs index 8249b59..8799703 100644 --- a/.check.exs +++ b/.check.exs @@ -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"]} ] diff --git a/.credo.exs b/.credo.exs index d03fbc5..5266b44 100644 --- a/.credo.exs +++ b/.credo.exs @@ -169,6 +169,7 @@ {Credo.Check.Refactor.ModuleDependencies, [ excluded_namespaces: [ + "Fotohaecker.Application", "Fotohaecker.Content", "FotohaeckerWeb", "FotohaeckerWeb.Endpoint", diff --git a/.gitignore b/.gitignore index 6a3d10c..0babc99 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,7 @@ fotohaecker-*.tar /priv/static/uploads/* # Ignore swagger files -/priv/static/api.json +/priv/static/schema.json # Ignore digested assets cache. /priv/static/cache_manifest.json diff --git a/config/config.exs b/config/config.exs index babac84..d0480d1 100644 --- a/config/config.exs +++ b/config/config.exs @@ -81,7 +81,7 @@ config :ueberauth, Ueberauth.Strategy.Auth0.OAuth, # Configure Swagger config :fotohaecker, :phoenix_swagger, swagger_files: %{ - "priv/static/api.json" => [ + "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. diff --git a/config/dev.exs b/config/dev.exs index 298eb79..1138be3 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -17,7 +17,7 @@ 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], - # FIXME: dont put url here + # TODO: test this url doesn't break anything url: [host: "localhost", port: 1337], static_url: [path: "/fh"], check_origin: false, diff --git a/config/prod.exs b/config/prod.exs index a7d344c..901826a 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -11,8 +11,7 @@ import Config # before starting your production server. config :fotohaecker, FotohaeckerWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json", - # FIXME: dont put url here - url: [host: "fschoenf.uber.space", port: 443] + url: [host: System.get_env("PHX_HOST"), port: 443] # Do not print debug messages in production config :logger, level: :info diff --git a/lib/fotohaecker/application.ex b/lib/fotohaecker/application.ex index 2f1f188..9735228 100644 --- a/lib/fotohaecker/application.ex +++ b/lib/fotohaecker/application.ex @@ -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] diff --git a/lib/fotohaecker_web/controllers/error_json.ex b/lib/fotohaecker_web/controllers/error_json.ex index d2eec57..12ae4f0 100644 --- a/lib/fotohaecker_web/controllers/error_json.ex +++ b/lib/fotohaecker_web/controllers/error_json.ex @@ -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: [ diff --git a/lib/fotohaecker_web/controllers/photo_controller.ex b/lib/fotohaecker_web/controllers/photo_controller.ex index f2ed86e..eca15fc 100644 --- a/lib/fotohaecker_web/controllers/photo_controller.ex +++ b/lib/fotohaecker_web/controllers/photo_controller.ex @@ -9,20 +9,7 @@ defmodule FotohaeckerWeb.PhotoController do def swagger_definitions do %{ - Photo: - swagger_schema do - title("Photo") - description("A photo") - - properties do - id(:integer, "Photo ID", required: true) - title(:string, "Photo title", required: true) - 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 + Photo: FotohaeckerWeb.PhotoJSON.schema() } end @@ -32,7 +19,9 @@ defmodule FotohaeckerWeb.PhotoController do response(200, "OK", Schema.ref(:Photo)) end - # TODO: result pagination + # 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) @@ -55,6 +44,21 @@ defmodule FotohaeckerWeb.PhotoController do 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) @@ -63,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 diff --git a/lib/fotohaecker_web/controllers/photo_json.ex b/lib/fotohaecker_web/controllers/photo_json.ex index 2823752..fa981e1 100644 --- a/lib/fotohaecker_web/controllers/photo_json.ex +++ b/lib/fotohaecker_web/controllers/photo_json.ex @@ -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. @@ -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. diff --git a/lib/fotohaecker_web/router.ex b/lib/fotohaecker_web/router.ex index 5e0af49..8c5867e 100644 --- a/lib/fotohaecker_web/router.ex +++ b/lib/fotohaecker_web/router.ex @@ -12,6 +12,7 @@ defmodule FotohaeckerWeb.Router do pipeline :api do plug CORSPlug + plug PhoenixSwagger.Plug.Validate plug :accepts, ["json"] end @@ -24,7 +25,7 @@ defmodule FotohaeckerWeb.Router do scope "/api/swagger", PhoenixSwagger do forward "/", Plug.SwaggerUI, otp_app: :fotohaecker, - swagger_file: "api.json" + swagger_file: "schema.json" end scope "/api", FotohaeckerWeb do @@ -38,6 +39,7 @@ defmodule FotohaeckerWeb.Router do scope "/search" do scope "/photos" do get "/:search", PhotoController, :search + get "/", PhotoController, :search_query end end end diff --git a/lib/fotohaecker_web/schema_behaviour.ex b/lib/fotohaecker_web/schema_behaviour.ex new file mode 100644 index 0000000..982f6a2 --- /dev/null +++ b/lib/fotohaecker_web/schema_behaviour.ex @@ -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 diff --git a/test/fotohaecker_web/controllers/photo_controller_test.exs b/test/fotohaecker_web/controllers/photo_controller_test.exs index cf8384f..3f9be6d 100644 --- a/test/fotohaecker_web/controllers/photo_controller_test.exs +++ b/test/fotohaecker_web/controllers/photo_controller_test.exs @@ -124,7 +124,7 @@ defmodule FotohaeckerWeb.PhotoControllerTest do end describe "search" do - test "finds photo", %{conn: conn} do + test "finds photo (deprecated endpoint)", %{conn: conn} do Mox.expect(Fotohaecker.UserManagement.UserManagementMock, :search!, fn _term -> [] end) @@ -166,7 +166,51 @@ defmodule FotohaeckerWeb.PhotoControllerTest do assert actual == expected end - test "returns empty list when empty string provided as input", %{conn: conn} do + test "finds photo", %{conn: conn} do + Mox.expect(Fotohaecker.UserManagement.UserManagementMock, :search!, fn _term -> + [] + end) + + photo_fixture(%{ + id: 1, + title: "scottish coast", + tags: ["scotland", "coast"] + }) + + photo_fixture(%{ + id: 2, + title: "spain" + }) + + actual = + conn + |> get(~p"/fh/api/search/photos?query=scotland") + |> json_response(200) + + expected = %{ + "data" => [ + %{ + "id" => 1, + "links" => %{"html" => "http://localhost:4002/fh/en_US/photos/1"}, + "tags" => ["scotland", "coast"], + "title" => "scottish coast", + "urls" => %{ + "full" => "http://localhost:4002/uploads/some_file_name_preview.jpg", + "raw" => "http://localhost:4002/uploads/some_file_name_og.jpg", + "thumb1x" => "http://localhost:4002/uploads/some_file_name_thumb@1x.jpg", + "thumb2x" => "http://localhost:4002/uploads/some_file_name_thumb@2x.jpg", + "thumb3x" => "http://localhost:4002/uploads/some_file_name_thumb@3x.jpg" + } + } + ] + } + + assert actual == expected + end + + test "returns empty list when empty string provided as input (deprecated endpoint)", %{ + conn: conn + } do actual = conn |> get(~p"/fh/api/search/photos/%20") @@ -178,6 +222,34 @@ defmodule FotohaeckerWeb.PhotoControllerTest do assert actual == expected end + + test "returns empty list when empty string provided as input", %{ + conn: conn + } do + actual = + conn + |> get(~p"/fh/api/search/photos?query=%20") + |> json_response(200) + + expected = %{ + "data" => [] + } + + assert actual == expected + end + + test "returns error when unknown parameter is provided", %{conn: conn} do + actual = + conn + |> get(~p"/fh/api/search/photos?unknown=123") + |> json_response(400) + + expected = %{ + "error" => %{"message" => "Required property query was not present.", "path" => "#"} + } + + assert actual == expected + end end # describe "create photo" do