defmodule SayCheezEx.Graphs.Provider do
@moduledoc """
The interface for a graph provider.
- checks if a such a provider is available, given the
providers' own configuration
- renders a graph to some HTML that can be embedded
"""
@callback render(String.t()) :: {:ok, String.t()} | {:error, String.t()}
@callback render!(String.t()) :: String.t()
@doc """
Runs an external command.
It returns a binary that captures STDOUT+STDERR if all went well,
or a tuple `{:error, e}` if something went bonkers.
If it's trying to call a command that does not exist on your
local environment, returns `{:error, :cmd_not_found}`.
"""
def run_cmd(cmd, parameters) do
try do
case System.cmd(cmd, parameters, stderr_to_stdout: true) do
{result, 0} ->
String.trim(result)
e ->
{:error, e}
end
rescue
e ->
case e do
%ErlangError{original: :enoent} -> {:error, :cmd_not_found}
_ -> {:error, e}
end
end
end
@doc """
Given a recipe *recipe* for a graph, first tries a cache and
then call a "builder" function to create it.
So if the SVG was already generated, we load it from
disk - if not, we build it again.
## Builder
It returns {:ok, svg_text} or {:error, reason}
that will be used to display a result.
"""
def rebuild_if_needed(module, recipe, fnBuilder) do
cache =
case cached?(module, recipe) do
{:miss} -> fnBuilder.(recipe)
{:hit, result} -> {:cached, result}
end
case cache do
{:error, e} ->
with :ok <- IO.puts("Error: #{inspect(e)}") do
{:error, e}
end
{:cached, v} ->
{:ok, v}
{:ok, v} ->
with :ok <- write_to_cache(module, recipe, v) do
{:ok, v}
end
end
end
@doc """
Computes a printable SHA for a string.
"""
def string_hash(s),
do:
:crypto.hash(:sha, s)
|> Base.encode16()
@spec file(:cache | :temp, binary(), binary()) :: binary
@doc """
Creates a file name for a temporary file.
Ths may be a cache file or a proper temporary file.
If you need mode than one file, you should encode it in
the extensions.
"""
def file(mode, hash, ext) do
dir =
case mode do
:temp ->
System.tmp_dir!()
:cache ->
with td <- "_build/img_cache/" do
File.mkdir_p!(td)
td
end
end
filename = "SayCheezImg_#{hash}_#{ext}"
Path.join(dir, filename)
end
@doc """
Creates a temporary file with the contents given,
returning the filename.
iex > Provider.to_temp_file("hello", "example")
"/temp/SayCheezImg_AAF4C61DDCC54D_tmp_example"
We make it unique based on the contents, and we
require a reason (for example one may have an input file
and an output file to generate the contents that will be cached).
"""
def to_temp_file(file_contents, reason) do
hash = string_hash(file_contents)
filename = file(:temp, hash, "tmp_#{reason}")
File.write!(filename, file_contents)
filename
end
@doc """
The only significant advantage of this HTTP client is that
it only uses things that are in Erlang itself.
https://stackoverflow.com/questions/20108421/using-the-httpc-erlang-module-from-elixir
"""
def trivial_http_get_client(url) do
:inets.start()
:ssl.start()
# {:ok,
# {{'HTTP/1.1', 200, 'OK'},
# [{'cache-control', 'max-age=600'}, {'connection', 'keep-alive'}, {'date', 'Sun, 13 Aug 2023 13:36:44 GMT'}, {'via', '1.1 varnish'}, {'accept-ranges', 'bytes'}, {'age', '0'}, {'etag', '"64d2c230-62a1"'}, {'server', 'GitHub.com'}, {'vary', 'Accept-Encoding'}, {'content-length', '25249'}, {'content-type', 'text/html; charset=utf-8'}, {'expires', 'Sat, 12 Aug 2023 21:13:32 GMT'}, {'last-modified', 'Tue, 08 Aug 2023 22:31:12 GMT'}, {'access-control-allow-origin', '*'}, {'x-proxy-cache', 'MISS'}, {'x-github-request-id', '50FE:1E84:3A10F7B:3B910FF:64D7F3A4'}, {'x-served-by', 'cache-fra-eddf8230046-FRA'}, {'x-cache', 'HIT'}, {'x-cache-hits', '1'}, {'x-timer', 'S1691933804.488448,VS0,VE91'}, {'x-fastly-request-id', '33838028b664f07c1371417b9755ce55472c970a'}],
# '\n\n
Elixir is a dynamic, functional language for building scalable and maintainable applications.
\n\n
Elixir runs on the Erlang VM, known for creating low-latency, distributed, and fault-tolerant systems. These capabilities and Elixir tooling allow developers to be productive in several domains, such as web development, embedded software, machine learning, data pipelines, and multimedia processing, across a wide range of industries.
\n\n
Here is a peek:
\n\n {:error, e}
end
end
@doc """
Checks whether we have a file in our cache.
If we do, we return its contents.
"""
def cached?(module, recipe) do
filename = cached_filename(module, recipe)
if File.exists?(filename) do
{:hit, File.read!(filename)}
else
{:miss}
end
end
@doc """
Write a file to its right cache.
"""
def write_to_cache(module, recipe, contents) do
filename = cached_filename(module, recipe)
File.write!(filename, contents)
end
@doc """
Generates a file name for a tuple (module, recipe).
"""
def cached_filename(module, recipe) do
hash = string_hash(recipe)
file(:cache, hash, "#{module}.cache")
end
@doc """
Earmark says that "A HTML Block defined by a tag starting a line
and the same tag starting a different line is parsed as one HTML
AST node, marked with %{verbatim: true}"
(see https://hexdocs.pm/earmark_parser/EarmarkParser.html )
So we wrap everything in a DIV and call it a day.
"""
def wrap_in_div_for_valid_markdown(s),
do: "
\n\n#{s}\n\n
\n\n"
@doc """
We want to simplify the SVG that Graphviz generates
so we can embed it in our HTML.
"""
def clean_up_svg(svg) do
svg
# XML header
|> String.replace(~r/<\?(.|\s)*?\?>/, "")
# doctype
|> String.replace(~r//, "")
# HTML comments
|> String.replace(~r//, "")
end
@doc """
Formats the error message, if any.
"""
def display({:ok, markdown}), do: markdown
def display({:error, msg}),
do: """
**Something went wrong**
#{msg}
"""
end