defmodule JSON.LD.DocumentLoader.Default do @moduledoc """ Default implementation of the `JSON.LD.DocumentLoader` behaviour. """ @behaviour JSON.LD.DocumentLoader alias JSON.LD.DocumentLoader.RemoteDocument alias JSON.LD.Options @spec load(String.t(), Options.t()) :: {:ok, RemoteDocument.t()} | {:error, any} def load(url, _options) do with {:ok, res} <- http_get(url), {:ok, data} <- Jason.decode(res.body) do {:ok, %RemoteDocument{document: data, document_url: res.request_url}} end end @spec http_get(String.t()) :: {:ok, HTTPoison.Response.t() | HTTPoison.AsyncResponse.t()} | {:error, any} defp http_get(url) do with {:ok, response} <- HTTPoison.get(url, [accept: "application/ld+json"], follow_redirect: true) do case url_from_link_header(response) do nil -> {:ok, response} url -> http_get(url) end end rescue e -> {:error, "HTTPoison failed: #{inspect(e)}"} end @spec url_from_link_header(HTTPoison.Response.t()) :: String.t() | nil defp url_from_link_header(response) do response.headers |> Enum.find(fn {name, _} -> name == "Link" end) |> case do nil -> nil {"Link", content} -> with {url, props} <- parse_link_header(content) do if match?(%{"rel" => "alternate", "type" => "application/ld+json"}, props) do if String.starts_with?(url, "http") do url else # Relative path response.request.url |> URI.parse() |> Map.put(:path, url) |> URI.to_string() end end end end end @spec parse_link_header(String.t()) :: {String.t(), map()} | nil defp parse_link_header(content) do [first | prop_strings] = String.split(content, ~r/\s*;\s*/) with [_, url] <- Regex.run(~r/\A<([^>]+)>\Z/, first) do props = Map.new(prop_strings, fn prop_string -> [_, key, value] = Regex.run(~r/\A([^=]+)=\"([^\"]+)\"\Z/, prop_string) {key, value} end) {url, props} end end end