# `IIIFImagePlug.V3`
[🔗](https://codeberg.org/dainst/iiif_image_plug/src/tag/1.0.1/lib/iiif_image_plug/v3.ex#L1)

This plug implements the IIIF Image API version 3 (see also https://iiif.io/api/image/3.0).

# `data_call`
*optional* 

```elixir
@callback data_call(conn :: Plug.Conn.t()) ::
  {:continue, Plug.Conn.t()} | {:stop, Plug.Conn.t()}
```

__Optional__ callback function that gets triggered at the start of each image data request, before
any processing is done.

If you want the plug to continue processing the data request, return `{:continue, conn}`,
otherwise you might instruct the plug to stop further processing by returning `{:stop, conn}`.

This could be used in conjunction with `c:IIIFImagePlug.V3.data_response/3` to implement your
own caching strategy.

_(naive!) Example_

    @impl true
    def data_call(conn) do
      path = MyApp.get_cache_path(conn)

      if File.exists?(path) do
        {:stop, Plug.Conn.send_file(conn, 200, path)}
      else
        {:continue, conn}
      end
    end

    @impl true
    def data_response(%Plug.Conn{} = conn, %Vix.Vips.Image{} = image, _format) do
      path = MyApp.get_cache_path(conn)

      path
      |> Path.dirname()
      |> File.mkdir_p!()

      Vix.Vips.Image.write_to_file(image, path)

      {:stop, send_file(conn, 200, path)}
    end

# `data_metadata`

```elixir
@callback data_metadata(identifier :: String.t()) ::
  {:ok, IIIFImagePlug.V3.DataRequestMetadata.t()}
  | {:error, IIIFImagePlug.V3.RequestError.t()}
```

__Required__ callback function triggered on image data requests, that maps the given _identifier_ to an
image file.

## Returns

- `{:ok, data_metadata}` on success, where `data_metadata` is a `IIIFImagePlug.V3.DataRequestMetadata` struct.
- `{:error, request_error}` otherwise, where `request_error` is a `IIIFImagePlug.V3.RequestError` struct.

## Example

    def data_metadata(identifier) do
      MyApp.ContextModule.get_image_path(identifier)
      |> case do
        {:ok, path} ->
          {
            :ok,
            %IIIFImagePlug.V3.DataRequestMetadata{
              path: path,
              response_headers: [
                {"cache-control", "public, max-age=31536000, immutable"}
              ]
            }
          }
        {:error, :not_found} ->
          {
            :error,
            %IIIFImagePlug.V3.RequestError{
              status_code: 404,
              msg: :not_found
            }
          }
      end
    end

# `data_response`
*optional* 

```elixir
@callback data_response(
  conn :: Plug.Conn.t(),
  image :: Vix.Vips.Image.t(),
  format :: atom()
) ::
  {:continue, Plug.Conn.t()} | {:stop, Plug.Conn.t()}
```

__Optional__ callback function that is triggered right before the final image gets rendered and sent.

This could be used in conjunction with `c:IIIFImagePlug.V3.data_call/1` to implement your
own caching strategy.

# `host`

```elixir
@callback host() :: String.t() | nil
```

__Optional__ callback function to override the `:host` evaluated from the `Plug.Conn`, useful if your Elixir app runs behind a proxy.

## Example

    def host(), do: "images.example.org"

# `info_call`
*optional* 

```elixir
@callback info_call(conn :: Plug.Conn.t()) ::
  {:continue, Plug.Conn.t()} | {:stop, Plug.Conn.t()}
```

__Optional__ callback function that gets triggered at the start of an image information request,
before any further evaluation is done.

If you want the plug to continue processing the information request, return `{:continue, conn}`,
otherwise you might instruct the plug to stop further processing by returning `{:stop, conn}`.
This can be used in conjunction with `c:IIIFImagePlug.V3.info_response/2` to implement your
own caching strategy.

_(naive!) Example_

    @impl true
    def info_call(conn) do
      path = MyApp.get_cache_path(conn)

      if File.exists?(path) do
        {:stop, Plug.Conn.send_file(conn, 200, path)}
      else
        {:continue, conn}
      end
    end

    @impl true
    def info_response(conn, info) do
      path = MyApp.get_cache_path(conn)

      path
      |> Path.dirname()
      |> File.mkdir_p!()

      File.write!(path, Jason.encode!(data))

      {:stop, send_file(conn, 200, path)}
    end

# `info_metadata`

```elixir
@callback info_metadata(identifier :: String.t()) ::
  {:ok, IIIFImagePlug.V3.InfoRequestMetadata.t()}
  | {:error, IIIFImagePlug.V3.RequestError.t()}
```

__Required__ callback function triggered on information requests (`info.json`), that maps the given _identifier_ to an
image file.

## Returns

- `{:ok, info_metadata}` on success, where `info_metadata` is a `IIIFImagePlug.V3.InfoRequestMetadata` struct.
- `{:error, request_error}` otherwise, where `request_error` is a `IIIFImagePlug.V3.RequestError` struct.

## Example

    def info_metadata(identifier) do
      MyApp.ContextModule.get_image_metadata(identifier)
      |> case do
        %{path: path, rights_statement: rights} ->
          {
            :ok,
            %IIIFImagePlug.V3.InfoRequestMetadata{
              path: path,
              rights: rights
            }
          }
        {:error, :not_found} ->
          {
            :error,
            %IIIFImagePlug.V3.RequestError{
              status_code: 404,
              msg: :not_found
            }
          }
      end
    end

# `info_response`
*optional* 

```elixir
@callback info_response(conn :: Plug.Conn.t(), info :: map()) ::
  {:continue, Plug.Conn.t()} | {:stop, Plug.Conn.t()}
```

__Optional__ callback function that gets triggered right before the `info.json` gets sent.

This could be used in conjunction with `c:IIIFImagePlug.V3.info_call/1` to implement your
own caching strategy.

# `port`

```elixir
@callback port() :: pos_integer() | nil
```

__Optional__ callback function to override the `:port` evaluated from the `Plug.Conn`, useful if your Elixir app runs behind a proxy.

## Example

    def port(), do: 1337

# `scheme`

```elixir
@callback scheme() :: String.t() | nil
```

__Optional__ callback function to override the `:scheme` ("http" or "https") evaluated from the `Plug.Conn`, useful if your Elixir app runs behind a
proxy.

## Example

    def scheme(), do: "https"

# `send_error`

```elixir
@callback send_error(
  conn :: Plug.Conn.t(),
  status_code :: number(),
  msg :: atom()
) :: Plug.Conn.t()
```

__Optional__ callback function that lets you override the default plug error response.

## Examples

__Default implementation__

The default response for all errors is defined as follows:

    def send_error(conn, status_code, msg) do
      conn
      |> Plug.Conn.put_resp_content_type("application/json")
      |> Plug.Conn.send_resp(
        status_code,
        Jason.encode!(%{error: msg})
      )
    end

__Rewriting 404 for data requests to serve a fallback image__

You can pattern match on specific `conn`, `status_code` or `msg` to overwrite specific cases.

One use case might be sending your own placeholder image instead of the JSON for failed data requests.

First customize your `data_metadata/1` implementation with a specific message (you do not want
to return an image on a failed `info.json` request):

    def data_metadata(identifier) do
      MyApp.ContextModule.get_image_path(identifier)
      |> case do
        {:ok, path} ->
          (...)
        {:error, :not_found} ->
          {
            :error,
            %IIIFImagePlug.V3.RequestError{
              status_code: 404,
              msg: :data_metadata_not_found
            }
          }
      end
    end

Then add a custom `send_error/3` that picks up on the status code and message you defined:

    def send_error(conn, 404, :data_metadata_not_found) do
      Plug.Conn.send_file(conn, 404, "#{Application.app_dir(:my_app)}/images/not_found.webp")
    end

For all errors that do not match the pattern, the plug will be falling back to the default implementation
shown above.

__Rewriting errors generated by the plug__

This also works for errors the plug generates interally:

    def send_error(conn, 400, :invalid_rotation) do
      requested_rotation = MyApp.extract_iiif_parameter(conn, :rotation)

      conn
      |> Plug.Conn.put_resp_content_type("application/json")
      |> Plug.Conn.send_resp(
        400,
        Jason.encode!(%{error: "Your rotation parameter '#{requested_rotation}' is invalid!"})
      )
    end

---

*Consult [api-reference.md](api-reference.md) for complete listing*
