DEV Community

juffel
juffel

Posted on

6 3

Customize sparql ✨ client middleware with tesla ⚡️

Tesla is a great elixir library that makes it pretty straightforward to write well-structured, but mighty http clients. One feature that makes tesla especially nice to use, is the plug-based middleware. Similar to Phoenix.Router you can just throw in a single line plug Tesla.Middleware.JSON and your client will automatically take care of encoding & decoding request & response bodies.

Tesla comes with a variety of ready-to-use middlewares, e.g. for adding headers or setting a base url for all requests. While those included middlewares are very handy, chances are that you'll want something more custom-made at some point. Luckily you can simply create a custom middleware and plug it into your existing tesla client.

In my case, I needed a database-backed log to save requests with their query parameters & the response body. While the official tesla docs contain an example for custom middlewares it took me a couple of attempts to get it up and running properly. So here's my take on custom middlewares with tesla 💫

Starting with this simple client that queries the wikidata SPARQL query api.

defmodule Wikidata.Client do
  use Tesla

  plug Tesla.Middleware.BaseUrl, "https://query.wikidata.org"
  plug Tesla.Middleware.Headers, [{"accept", "application/sparql-results+json"}]
  plug Wikidata.SaveClientResponse

  @foods_query File.read!("lib/wikidata/queries/foods.sparql")

  defp get_response() do
    with {:ok, response} <- get("/sparql", query: [query: @foods_query]) do
      {:ok, unpack_response(response.body)}
    else
      {:error, :timeout} ->
        {:error, :wikidata_client_timeout}
      error ->
        {:error, :wikidata_client_error, error}
    end
  end

  defp unpack_response(body) do
    body
    |> Jason.decode!(keys: :atoms)
    |> Map.get(:results)
    |> Map.get(:bindings)
  end
end
Enter fullscreen mode Exit fullscreen mode

Please ignore all the wikidata/sparql-related stuff, if you're not interested in that part - I just included it to give the example some relevance (and check out the post's end for the actual sparql request in case you're interested).

The line plug Wikidata.SaveClientResponse adds a custom module to the tesla middleware pipeline. A custom middleware needs to implement the Tesla.Middleware behaviour which is as simple as a module which implements a call function with this spec:

@spec call(env :: Tesla.Env.t(), next :: Tesla.Env.stack(), options :: any()) :: Tesla.Env.result()
Enter fullscreen mode Exit fullscreen mode

To store the request data, I use a simple ecto schema ClientRequest:

defmodule Wikidata.ClientRequest do
  use Ecto.Schema
  import Ecto.Changeset

  schema "wikidata_client_requests" do
    field :query, :string
    field :response_body, :string

    timestamps()
  end

  def create_changeset(attrs) do
    %__MODULE__{}
    |> cast(attrs, [:query, :response_body])
  end
end
Enter fullscreen mode Exit fullscreen mode

And finally the actual middleware implementation which intercepts requests and stores them as ClientRequests in the database could look like this:

defmodule Wikidata.SaveClientResponse do
  @behaviour Tesla.Middleware

  alias Wikidata.ClientRequest

  @impl Tesla.Middleware
  def call(env, next, _options) do
    with {:ok, env} <- Tesla.run(env, next) do
      save_request_data(env)

      {:ok, env}
    else
      result -> result
    end
  end

  defp save_request_data(%{query: [query: query], body: body}) do
    %{query: query, response_body: body}
    |> ClientRequest.create_changeset()
    |> Repo.insert()
  end
end
  defp save_request_data(_), do: nil
Enter fullscreen mode Exit fullscreen mode

This simple middleware will save a new ClientRequest for each successful request, which includes a query param named query and returns with a body, and will just hand through all other requests of the client (i.e. timed out requests, or any request which does not include a query param). It's just a basic serving suggestion, so please adjust to taste 🍲
I.e. you want to access query params before the request is sent out? Then you'll have to access (or alter) env before you call Tesla.run/2


[1] In case you're interested in my actual use case, here's the wikidata sparql query, which fetches all foods/food ingredients with their descriptions, pictures and related food classes or instances:

SELECT ?item ?itemLabel ?itemDescription ?imageUrl ?instanceOf
WHERE
{
  ?item wdt:P31*/wdt:P279* wd:Q25403900.
  OPTIONAL { ?item wdt:P18 ?imageUrl }
  OPTIONAL { ?item wdt:P31 ?instanceOf }
  SERVICE wikibase:label { bd:serviceParam wikibase:language "en". }
}
Enter fullscreen mode Exit fullscreen mode

Image of Docusign

Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs