defmodule Contentful.Delivery do @moduledoc """ The Delivery API is the main access point for fetching data for your customers. The API is _read only_. If you wish to manipulate data, please have a look at the `Contentful.Management`. ## Basic interaction The `space_id`, the `environment` and your `access_token` can all be configured in `config/config.exs`: ``` # config/config.exs config :contentful, delivery: [ space_id: "", environment: "", access_token: "" ] ``` The space can then be fetched as a `Contentful.Space` via a simple query: ``` import Contentful.Query alias Contentful.Delivery.Spaces {:ok, space} = Spaces |> fetch_one ``` Retrieving items is then just a matter of importing `Contentful.Query`: ``` import Contentful.Query alias Contentful.Delivery.Entries {:ok, entries, total: _total_count_of_entries} = Entries |> fetch_all ``` You can create query chains to form more complex queries: ``` import Contentful.Query alias Contentful.Delivery.Entries {:ok, entries, total: _total_count_of_entries} = Entries |> skip(2) |> limit(10) |> include(2) |> fetch_all ``` Fetching indidvidual entities is straight forward: ``` import Contentful.Query alias Contentful.Delivery.Assets my_asset_id = "my_asset_id" {:ok, assets, total: _total_count_of_assets} = Assets |> fetch_one(my_asset_id) ``` All query resolvers also support chaning the `space_id`, `environment` and `access_token` at call time: ``` import Contentful.Query alias Contentful.Delivery.Assets my_asset_id = "my_asset_id" {:ok, asset} = Assets |> fetch_one(my_asset_id) ``` Note: If you want to pass the configuration at call time, you can pass these later as function parameters to the resolver call: ``` import Contentful.Query alias Contentful.Delivery.Assets my_asset_id = "my_asset_id" my_space_id = "bmehzfuz4raf" my_environment = "staging" my_access_token = "not_a_real_token" {:ok, asset} = Assets |> fetch_one(my_asset_id, my_space_id, my_environment, my_access_token) # also works for fetch_all: {:ok, assets, _} = Assets |> fetch_all(my_space_id, my_environment, my_access_token) # and for stream: [ asset | _ ] = Assets |> stream(my_space_id, my_environment, my_access_token) |> Enum.to_list ``` ## Spaces as an exception Unfortunately, `Contentful.Delivery.Spaces` do not support complete collection behaviour: ``` # doesn't exist in the Delivery API: {:error, _, _} = Contentful.Delivery.Spaces |> fetch_all # however, you can still retrieve a single `Contentful.Space`: {:ok, space} = Contentful.Delivery.Spaces |> fetch_one # the configured space {:ok, my_space} = Contentful.Delivery.Spaces |> fetch_one("my_space_id") # a passed space ``` ## Further reading * [Contentful Delivery API docs](https://www.contentful.com/developers/docs/references/content-delivery-api/) (CDA). """ import Contentful.Misc, only: [fallback: 2] import HTTPoison, only: [get: 2] alias Contentful.Configuration alias HTTPoison.Response @endpoint "cdn.contentful.com" @preview_endpoint "preview.contentful.com" @protocol "https" @separator "/" @doc """ Gets the json library for the Contentful Delivery API based on the config/config.exs. """ @spec json_library :: module() def json_library do Contentful.json_library() end @doc """ constructs the base url with protocol for the CDA ## Examples "https://cdn.contentful.com" = url() """ @spec url() :: String.t() def url do "#{@protocol}://#{host_from_config()}" end @doc """ constructs the base url with the space id that got configured in config.exs """ @spec url(nil) :: String.t() def url(space) when is_nil(space) do case space_from_config() do nil -> url() space -> space |> url end end @doc """ constructs the base url with the extension for a given space ## Examples "https://cdn.contentful.com/spaces/foo" = url("foo") """ @spec url(String.t()) :: String.t() def url(space) do [url(), "spaces", space] |> Enum.join(@separator) end @doc """ When explicilty given `nil`, will fetch the `environment` from the environments current config (see `config/config.exs`). Will fall back to `"master"` if no environment is set. ## Examples "https://cdn.contentful.com/spaces/foo/environments/master" = url("foo", nil) # With config set in config/config.exs config :contentful_delivery, environment: "staging" "https://cdn.contentful.com/spaces/foo/environments/staging" = url("foo", nil) """ @spec url(String.t(), nil) :: String.t() def url(space, env) when is_nil(env) do [space |> url(), "environments", environment_from_config()] |> Enum.join(@separator) end @doc """ constructs the base url for the delivery endpoint for a given space and environment ## Examples "https://cdn.contentful.com/spaces/foo/environments/bar" = url("foo", "bar") """ def url(space, env) do [space |> url(), "environments", env] |> Enum.join(@separator) end @doc """ Sends a request against the CDA. It's really just a wrapper around `HTTPoison.get/2` """ @spec send_request({binary(), any()}) :: {:error, HTTPoison.Error.t()} | {:ok, Response.t()} def send_request({url, headers}) do get(url, headers) end @doc """ Parses the response from the CDA and triggers a callback on success """ @spec parse_response({:ok, Response.t()}, fun()) :: {:ok, struct()} | {:ok, list(struct()), total: non_neg_integer()} | {:error, :rate_limit_exceeded, wait_for: integer()} | {:error, atom(), original_message: String.t()} def parse_response( {:ok, %Response{status_code: code, body: body} = resp}, callback ) do case code do 200 -> body |> json_library().decode! |> callback.() 401 -> body |> build_error(:unauthorized) 404 -> body |> build_error(:not_found) _ -> resp |> build_error() end end @doc """ catch_all for any errors during flight (connection loss, etc.) """ @spec parse_response({:error, any()}, fun()) :: {:error, :unknown} def parse_response({:error, _}, _callback) do build_error() end @doc """ Used to construct generic errors for calls against the CDA """ @spec build_error(String.t(), atom()) :: {:error, atom(), original_message: String.t()} def build_error(response_body, status) do {:ok, %{"message" => message}} = response_body |> json_library().decode() {:error, status, original_message: message} end @doc """ Used for the rate limit exceeded error, as it gives the user extra information on wait times """ @spec build_error(Response.t()) :: {:error, :rate_limit_exceeded, wait_for: integer()} def build_error(%Response{ status_code: 429, headers: [{"x-contentful-rate-limit-exceeded", seconds}, _] }) do {:error, :rate_limit_exceeded, wait_for: seconds} end @doc """ Used to make a generic error, in case the API Response is not what is expected """ @spec build_error() :: {:error, :unknown} def build_error do {:error, :unknown} end defp environment_from_config do Configuration.get(:environment) |> fallback("master") end defp space_from_config do Configuration.get(:space) end defp host_from_config do case Configuration.get(:endpoint) do nil -> @endpoint :preview -> @preview_endpoint value -> value end end end