qhwa

qhwa

Formular - A tiny DSL engine / code evaluator (configuration as code)

Hi, all,

I just published Formular package. It is a tiny library that evaluates a piece of Elixir code.

Online documentation

On the shoulder of Elixir’s Code module

Given a piece of Elixir code (as a string, or AST), Formular runs it with Elixir’s Code module under some security limitations.

So far, the limitations are:

  • No calling module functions;
  • No calling exit;
  • No sending messages.

Indeed, the whole library consists of only one thin module, thanks to the power Elixir has already shipped out of the box.

Motivation

Formular was developed to support some dynamic configuration scenarios. For example, in a scene of an online book store, the discount of a book can be dynamically configured as a piece of code, then evaluated by Formular:

iex> discount_formula = ~s"
...>   case order do
...>     # old books get a big promotion
...>     %{book: %{year: year}} when year < 2000 ->
...>       0.5
...>   
...>     %{book: %{tags: tags}} ->
...>       # Elixir books!
...>       if ~s{elixir} in tags do
...>         0.9
...>       else
...>         1.0
...>       end
...>
...>     _ ->
...>       1.0
...>   end
...> "
...>
...> book_order = %{
...>   book: %{
...>     title: "Elixir in Action", year: 2019, tags: ["elixir"]
...>   }
...> }
...>
...> Formular.eval(discount_formula, [order: book_order])
{:ok, 0.9}

In such a way, the discount calculation code, which changes frequently, is separated away from the stable business flow, and the primary code is probably more generic and flexible.

I’ve been using it in production for a while so I publish it today in case others may find it useful too.

Cheers!

Most Liked

hauleth

hauleth

DoS (atom exhaustion):

Formular.eval(~S|for a <- %Range{first: 0, last: 100_000, step: 1}, do: :"#{a}"|, [])

I needed to create range manually, as you do not export ../2 operator.

qhwa

qhwa

Two new versions have been released:

  • v0.2.2

    • brings performance improvements to 0.2.x
  • v0.3.0

    • allows limiting the execution time and heap size
    • allows compiling a code string to an Elixir module which brings more performance improvements,
    • adds new API Formula.used_vars/2 to extract the used variable names in the formula.

Highlights:

Performance improvements:

A simple benchmark on a simple formula results in this:

Name                      ips        average  deviation         median         99th %
compiled_module      947.84 K        1.06 μs  ±5536.01%        0.83 μs        1.45 μs
eval                  14.53 K       68.82 μs     ±8.93%       68.69 μs       87.93 μs
eval_ast               4.86 K      205.58 μs    ±18.43%      193.89 μs      333.89 μs

Comparison: 
compiled_module      947.84 K
eval                  14.53 K - 65.23x slower +67.76 μs
eval_ast               4.86 K - 194.85x slower +204.52 μs

The compilation approach is about 80x faster than the original eval approach in v0.2.2, and 300x faster than versions prior to v0.2.2.

Restricted evaluation:

  • limited execution time:

    Formular.eval(code, timeout: :timer.seconds(5))
    
  • limited max heap size by word:

    Formular.eval(code, max_heap_size: 15_000)
    

If limited, the evaluation will be run in a separate process.

Extract used vars API:

This can be helpful if you need to build some UI arround the formula.

iex> Formular.used_vars("a + b - 1")
[:a, :b]
mat-hek

mat-hek

Membrane Core Team

Nice, though I’m curious if you tried using Sand?

Where Next?

Popular in Announcing Top

Crowdhailer
Experimenting with this code. OK.try do user &lt;- fetch_user(1) cart &lt;- fetch_cart(1) order = checkout(cart, user) save_orde...
New
pkrawat1
Hey guyz We at @aviabird are working on a payment library in elixir/phoenix. We are targeting March 2018 to add 56 Gateways to it. Have...
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 30380 241
New
dominicletz
Hi, I thought I had posted my library before but seems I hadn’t. The project is still in early stages but it’s growing and so I think it...
New
zoltanszogyenyi
Hey everyone :waving_hand: Excited to join this forum - I am one of the founders and current project maintainers of a popular and open-s...
New
restlessronin
The repo is at GitHub - cyberchitta/openai_ex: Community maintained Elixir library for OpenAI API · GitHub. Docs are at OpenaiEx User Gu...
152 10739 134
New
bluzky
You may know https://ui.shadcn.com/, a UI component library for React. I really love it’s design style and components. I’ve built some co...
384 14585 119
New

Other popular topics Top

Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 130579 1222
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New

We're in Beta

About us Mission Statement