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 HTTPoison, only: [get: 2] alias HTTPoison.Response @endpoint "cdn.contentful.com" @preview_endpoint "preview.contentful.com" @protocol "https" @separator "/" @agent_header [ "User-Agent": "Contentful Elixir SDK" ] @accept_header [ accept: "application/json" ] @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 """ Builds the request headers for a request against the CDA, taking api access tokens into account ## Examples my_access_token = "foobarfoob4z" [ "Authorization": "Bearer foobarfoob4z", "User-Agent": "Contentful Elixir SDK", "Accept": "application/json" ] = my_access_token |> request_headers() """ @spec request_headers(String.t()) :: keyword() def request_headers(api_key) do api_key |> authorization_header() |> Keyword.merge(@agent_header) |> Keyword.merge(@accept_header) end @doc """ Sends a request against the CDA. It's really just a wrapper around `HTTPoison.get/2` """ @spec send_request(tuple()) :: {:ok, Response.t()} def send_request({url, headers}) do get(url, headers) end @doc """ Prevents parsing of empty options. ## Examples "" = collection_query_params([]) """ def collection_query_params([]) do "" end @doc """ parses the options for retrieving a collection. It will drop any option that is not in @collection_filters ([:limit, :skip]) ## Examples "?limit=50&skip=25&order=foobar" = collection_query_params(limit: 50, baz: "foo", skip: 25, order: "foobar", bar: 42) """ @spec collection_query_params( limit: pos_integer(), skip: non_neg_integer(), content_type: String.t(), include: non_neg_integer() ) :: String.t() def collection_query_params(options) do params = options |> Keyword.take([:limit, :skip, :content_type, :include]) |> URI.encode_query() "?#{params}" 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 authorization_header(token) when is_nil(token) do api_key_from_configuration() |> authorization_header() end defp authorization_header(token) do [authorization: "Bearer #{token}"] end defp api_key_from_configuration do config(:api_key) |> fallback("") end defp environment_from_config do config(:environment) |> fallback("master") end defp space_from_config do config(:space) end defp host_from_config do case config(:endpoint) do nil -> @endpoint :preview -> @preview_endpoint value -> value end end @doc """ Can be used to retrieve configuration for the `Contentful.Delivery` module ## Examples config :contentful, delivery: [ my_config: "foobar" ] "foobar" = Contentful.Delivery.config(:my_config) """ @spec config(atom()) :: any() def config(setting) do config() |> Keyword.get(setting) end @doc """ loads the configuration for the delivery module from the contentful app configuration """ @spec config() :: list(keyword()) def config do Application.get_env(:contentful, :delivery, []) end defp fallback(nil, value) do value end defp fallback(value, _) do value end end