defmodule SerpCheap do
@moduledoc """
Elixir client for the [serp.cheap](https://serp.cheap) Google Search API.
Configure it via application env (the Phoenix way):
config :serpcheap, api_key: System.get_env("SERPCHEAP_API_KEY")
Then call from anywhere — options can also be overridden per call:
{:ok, results} = SerpCheap.search("best running shoes", gl: "us")
"""
alias SerpCheap.Client
@type opts :: keyword()
@type response :: {:ok, map()} | {:error, SerpCheap.Error.t()}
@doc "Run a Google search. Retries transient errors (429/503/timeout) with backoff."
@spec search(String.t(), opts()) :: response()
def search(query, opts \\ []) when is_binary(query) do
payload =
%{"q" => query, "gl" => Keyword.get(opts, :gl, "us"), "page" => Keyword.get(opts, :page, 1)}
|> put(opts, :hl, "hl")
|> put(opts, :tbs, "tbs")
Client.request("/v1/search", payload, opts, &is_list(&1["organic"]))
end
@doc "Fetch and extract a single page (content + optional screenshot)."
@spec scrape(String.t(), opts()) :: response()
def scrape(url, opts \\ []) when is_binary(url) do
payload =
%{"url" => url}
|> put(opts, :render_js, "render_js")
|> put(opts, :screenshot, "screenshot")
|> put(opts, :wait_for, "wait_for")
|> put(opts, :wait_ms, "wait_ms")
Client.request("/v1/scrape", payload, opts, &is_binary(&1["url"]))
end
@doc "Find where a url/domain ranks for a keyword across Google result pages."
@spec rank(String.t(), String.t(), opts()) :: response()
def rank(url, query, opts \\ []) when is_binary(url) and is_binary(query) do
payload =
%{
"url" => url,
"q" => query,
"gl" => Keyword.get(opts, :gl, "us"),
"pages" => Keyword.get(opts, :pages, 1),
"match_type" => Keyword.get(opts, :match_type, "domain")
}
|> put(opts, :hl, "hl")
|> put(opts, :tbs, "tbs")
Client.request(
"/v1/rank",
payload,
opts,
&(is_list(&1["organic"]) and is_list(&1["matches"]))
)
end
@doc "Like `search/2` but returns the result or raises `SerpCheap.Error`."
@spec search!(String.t(), opts()) :: map()
def search!(query, opts \\ []), do: unwrap(search(query, opts))
@doc "Like `scrape/2` but returns the result or raises `SerpCheap.Error`."
@spec scrape!(String.t(), opts()) :: map()
def scrape!(url, opts \\ []), do: unwrap(scrape(url, opts))
@doc "Like `rank/3` but returns the result or raises `SerpCheap.Error`."
@spec rank!(String.t(), String.t(), opts()) :: map()
def rank!(url, query, opts \\ []), do: unwrap(rank(url, query, opts))
defp unwrap({:ok, value}), do: value
defp unwrap({:error, error}), do: raise(error)
defp put(payload, opts, key, field) do
case Keyword.get(opts, key) do
nil -> payload
value -> Map.put(payload, field, value)
end
end
end