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'}],
# '<!DOCTYPE html>\n<html xmlns="http://www.w3.org/1999/xhtml" lang="en">\n<head>\n <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />\n <meta http-equiv="X-UA-Compatible" content="IE=edge" />\n <meta name="description" content="Welcome to Elixir, a dynamic, functional language designed for building scalable and maintainable applications">\n <title>The Elixir programming language</title>\n <link href="https://elixir-lang.org/atom.xml" rel="alternate" title="Elixir\'s Blog" type="application/atom+xml" />\n <link rel="stylesheet" type="text/css" href="/css/style.css" />\n <link rel="stylesheet" type="text/css" href="/css/syntax.css" />\n <link rel="stylesheet" href="/js/icons/style.css">\n <!--[if lt IE 8]><!-->\n <link rel="stylesheet" href="/js/icons/ie7/ie7.css">\n <!--<![endif]-->\n <meta name="viewport" content="width=device-width,initial-scale=1" />\n <link rel="stylesheet" id="font-bitter-css" href="//fonts.googleapis.com/css?family=Bitter:400,700" type="text/css" media="screen" />\n <link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />\n <link rel="search" type="application/opensearchdescription+xml" title="elixir-lang.org" href="/opensearch.xml" />\n <script defer data-domain="elixir-lang.org" src="https://plausible.io/js/plausible.js"></script>\n <script defer src="//ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>\n <script defer src="/js/index.js" type="text/javascript" charset="utf-8"></script>\n <!-- Begin Jekyll SEO tag v2.8.0 -->\n<meta name="generator" content="Jekyll v3.9.3" />\n<meta property="og:title" content="elixir-lang.github.com" />\n<meta property="og:locale" content="en_US" />\n<meta name="description" content="Website for Elixir" />\n<meta property="og:description" content="Website for Elixir" />\n<link rel="canonical" href="https://elixir-lang.org/" />\n<meta property="og:url" content="https://elixir-lang.org/" />\n<meta property="og:site_name" content="elixir-lang.github.com" />\n<meta property="og:type" content="website" />\n<meta name="twitter:card" content="summary" />\n<meta property="twitter:title" content="elixir-lang.github.com" />\n<script type="application/ld+json">\n{"@context":"https://schema.org","@type":"WebSite","description":"Website for Elixir","headline":"elixir-lang.github.com","name":"elixir-lang.github.com","url":"https://elixir-lang.org/"}</script>\n<!-- End Jekyll SEO tag -->\n\n</head>\n\n<body class="home">\n <div id="container">\n <div class="wrap">\n <div id="header">\n <div id="branding">\n <a id="site-title" href="/" title="Elixir" rel="Home">\n <img class="logo" src="/images/logo/logo.png" alt="Elixir Logo" />\n </a>\n </div>\n\n <div id="menu-primary" class="menu-container">\n <ul id="menu-primary-items">\n <li class="menu-item home"><a class="spec" href="/">Home</a></li>\n <li class="menu-item install"><a class="spec" href="/install.html">Install</a></li>\n <li class="menu-item learning"><a class="spec" href="/learning.html">Learning</a></li>\n <li class="menu-item docs"><a class="spec" href="/docs.html">Docs</a></li>\n <li class="menu-item getting-started"><a class="spec" href="/getting-started/introduction.html">Guides</a></li>\n <li class="menu-item cases"><a class="spec" href="/cases.html">Cases</a></li>\n <li class="menu-item blog"><a class="spec" href="/blog/">Blog</a></li>\n </ul>\n </div>\n </div>\n\n <div id="main">\n\n\n<div id="content">\n <div class="hfeed">\n <div class="hentry post">\n <div class="entry-summary">\n <h5>Elixir is a dynamic, functional language for building scalable and maintainable applications.</h5>\n\n <p>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.</p>\n\n <p>Here is a peek:</p>\n\n<figure class="highl' ++ ...}}
case :httpc.request(String.to_charlist(url)) do
{:ok, {{_, _http_code, _}, _, body}} -> {:ok, "#{body}"}
e -> {: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: "<div>\n\n#{s}\n\n</div>\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/<!DOCTYPE(.|\s)*?>/, "")
# HTML comments
|> String.replace(~r/<!--(.|\s)*?-->/, "")
end
@doc """
Formats the error message, if any.
"""
def display({:ok, markdown}), do: markdown
def display({:error, msg}),
do: """
**Something went wrong**
#{msg}
"""
end