lib/url/data.ex

defmodule URL.Data do
  @moduledoc """
  Parses a `data` URL
  """
  import NimbleParsec
  import URL.ParseHelpers.{Core, Params, Unwrap}
  alias URL.ParseHelpers.Params

  @default_mediatype "text/plain"
  defstruct mediatype: @default_mediatype, params: %{}, data: ""

  @type t() :: %__MODULE__{
          mediatype: binary(),
          params: map(),
          data: String.t() | {:error, String.t()}
        }

  @doc """
  Parse a URI with the `:scheme` of "data"

  ## Example

      iex> data = URI.parse "data:text/plain;base64,SGVsbG8gV29ybGQh"
      iex> URL.Data.parse(data)
      {:ok,
       %URL.Data{
         mediatype: "text/plain",
         params: %{"encoding" => "base64"},
         data: "Hello World!"
       }}

  """
  @spec parse(URI.t()) :: {:ok, __MODULE__.t()} | {:error, {module(), binary()}}
  def parse(%URI{scheme: "data", path: nil}) do
    __MODULE__
    |> struct(data: "")
    |> Params.wrap(:ok)
  end

  def parse(%URI{scheme: "data", path: path}) do
    with {:ok, data} <- unwrap(parse_data(path)) do
      struct(__MODULE__, data)
      |> decode_data
      |> Params.wrap(:ok)
    end
  end

  defp decode_data(%__MODULE__{params: %{"encoding" => "base64"}, data: data} = url) do
    case Base.decode64(data) do
      {:ok, decoded} -> Map.put(url, :data, decoded)
      :error -> Map.put(url, :data, {:error, data})
    end
  end

  defp decode_data(%__MODULE__{} = data) do
    Map.put(data, :data, URI.decode(data.data))
  end

  defparsecp(
    :parse_data,
    optional(mediatype())
    |> concat(params())
    |> ignore(comma())
    |> concat(data())
  )
end