Skip to content

Instantly share code, notes, and snippets.

@franciscoj
Last active September 27, 2018 18:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save franciscoj/47de59f80c3cbe20c52882766e2bdf55 to your computer and use it in GitHub Desktop.
Save franciscoj/47de59f80c3cbe20c52882766e2bdf55 to your computer and use it in GitHub Desktop.
Overengineering a blog

Overengineering a blog (1)

I've had a domain parked for years waiting for it to be transformed on some sort of web business card. Since I had 2 weeks of holidays I thought it might be cool to spend some spare time playing with it.

I jumped into it and decided right away what I wanted to do:

  1. An Elixir + Phoenix app.
  2. With TypeScript + React + Tailwindcss on the frontend.

Yup, this is all a big ball of bullshit. I decided what do use and even a whole stack before I even decided on WHAT I wanted to do.

This stupid piece of bias inspired me to do something. I'm going to dramatically overengineer a blog and I'm going to try to have fun and learn something in the process. To do so I've decided to include some completely arbitrary limitations and features.

  1. Use the aforementioned stack (it was already setup :P).
  2. No piece of infrastructure beyond BEAM (as long as possible). e.g. no MySQL or PostgreSQL to store the posts).
  3. Use a format similar to Jekyll's YAML front matter + markdown + liquid templates. (I already have an old blog with posts on this format).
  4. No CRUD or human controlled interface. For example, I'll keep the posts on a GitHub repository and I will publish it by catching the push webhook, reading its content and parsing it on the server.

Overengineering a blog (2)

On the stack

I already had the stack mentioned in (1) prepared, so I'll avoid commenting on that. Just a normal Phoenix umbrella app without Ecto or Brunch and a small webpack setup for all the frontend goodies.

Also instead of having all the frontend source inside the Phoenix's app folder I separated it into a frontend folder.

Extracting the post parts

Beyond that, the next smaller part of the OEB (OverEngineered Blog) is to split the post file into the front matter and the markdown content.

It has this format:

---
key: value
key2: value2
---

# This is the title

This is the content

The part between the --- signs is the front matter which is YAML with some options about the post (The complete title, some tags and some other metadata).

I first thought about using Elixir's pattern matching and just separate the string.

Something like:

"---\n" <> front_matter <> "---\n\n" <> content = post

The problem is that something like that wouldn't work.

** (ArgumentError) the left argument of <> operator inside a match should be always a literal binary as its size can't be verified, got: front_matter

Meaning that all the parts need to have a known size in order to be parsed like that. I don't have a way to do so so the next easier solution I could imagine was to String.split("---\n") which just works like a charm.

So, TIL: To pattern match a string all parts need to have a fixed size (except for the last one, which is allowed to have a variable size). More info: https://thepugautomatic.com/2016/01/pattern-matching-complex-strings/

Update: It turned out that a regexp is even better and I can just use Regex.named_captures/3

The regular expression I used is:

~r"""
\A---$
(?<front_matter>.*)
^---$

(?<content>.*)
"""""""ms

Which after using Regex.named_captures returns a map with the form %{"front_matter" => "...", "content" => "..."} or nil in case the regular expression didn't match.

Overengineering a blog (3)

Parsing the front matter

As I said before on other post, the front matter is just a YAML header in the markdown files.

After a quick Google search I found a couple of solutions: yamerl and yaml-elixir.

I ended up using yamerl because it seemed simpler and then transforming the list of tuples it returns into a map.

The transformation is pretty easy because Map.new/1 can do it.

iex(1)> Map.new([{"hello", "world"}, {"hola", "mundo"}])
%{"hello" => "world", "hola" => "mundo"}

However the problem is that yamerl returns a list of tuples whose components are not strings but charlist. This made me think on switching to yaml-elixir instead because it manages this kind of things, but I ended up solving it on my own. Traversing a structure like this (and taking into account that my YAML will be pretty simple) is easy.

So I ended up creating this function

def stringify([]), do: []

def stringify([{key, value} | rest]),
  do: [{to_string(key), stringify(value)} | stringify(rest)]

def stringify([value | rest]) when is_list(value),
  do: [to_string(value) | stringify(rest)]

def stringify(value), do: to_string(value)

that won't cover all cases, but covers my case, which having something like:

- key: value
- key2:
    - value2
    - value3

PS: I also made a small [yamerl-pr][good-citizen-pr] to yamerl to upgrade their Elixir doc to show the latest version of the lib.

Overengineering a blog (4)

Adding dialyzer

dialyzer, a DIscrepancy AnaLYZer for ERlang programs.

What does that mean? Well, dialyzer is a static analysis tool for your code. It will check for discrepancies such as type errors, unreachable code, etc.

That's a really useful thing to have while coding because it will make some of your bugs visible.

dialyzer, as it names says, is a tool for for Erlang programs, and to install it on Elixir there are a couple of options, but the most popular is dialyxir.

Installing it seems to be pretty easy... except it isn't.

After just following the instructions I had a lot of erros like these

:dialyzer.run error: Analysis failed with error:
Could not scan the following file(s):
  Could not get Core Erlang code for: /home/myuser/.asdf/installs/elixir/1.7.3/lib/elixir/ebin/Elixir.Inspect.Version.Requirement.beam
  Recompile with +debug_info or analyze starting from source code  Could not get Core Erlang code for: /home/fran/.asdf/installs/elixir/1.7.3/lib/elixir/ebin/elixir_erl_try.beam
  Recompile with +debug_info or analyze starting from source code  Could not get Core Erlang code for: /home/fran/.asdf/installs/elixir/1.7.3/lib/elixir/ebin/elixir_rewrite.beam

Luckily for me there's a ton of amazing people in the [elixir-forums][elixir forums].

After installing the right Elixir 1.7.3 version as explained in that forum message I can already run mix dialyzer.

done (passed successfully)

Perfect! Let's now add some error to see how it works. What if we had something like this?

def foo(), do: nil

def bar() do
  %{name: name} = foo()

  IO.puts(name)
end

Dialyzer would tell us something like:

apps/myapp/lib/my_file.ex:23:no_return
Function bar/0 has no local return.
________________________________________________________________________________
apps/myapp/lib/my_file.ex:24:pattern_match
The pattern
%{:name => _name}

can never match the type
nil

If I fix foo to: def foo(), do: %{name: "Fran"} Dialyzer shows no error. Nice and useful!

Good thing is that Dialyzer will also be able to read our typespecs and provide more useful information.

@andresgutgon
Copy link

Nice! Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment