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\n \n \n \n The Elixir programming language\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n
\n \n\n
\n\n\n
\n
\n
\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