← Articoli

Kumi: logica di calcolo in Ruby che gira anche in JavaScript

Molti dei miei progetti sono webshop e sistemi di fatturazione. In ognuno di essi si ripresenta lo stesso problema: la logica dei prezzi vive sul server, ma il cliente vuole vedere il totale nel browser mentre modifica le quantità. Così le regole vengono scritte due volte, una in Ruby e una in JavaScript, e prima o poi le due versioni divergono.

Kumi di André Muta è una gem Ruby che prende sul serio questo problema. È un DSL dichiarativo per la logica di calcolo, come regole fiscali, prezzi e punteggi, che da un unico schema compila in Ruby semplice e in JavaScript semplice. L’ho provato, e quello che vedo mi piace molto.

Cosa fa Kumi

Descrivete la forma dei dati in ingresso e i valori che volete calcolare. Non scrivete cicli né decidete l’ordine di valutazione. Il compilatore ricava le dipendenze, controlla i tipi e genera per ogni target codice lineare, senza dipendenze.

Due idee lo distinguono dallo scrivere semplici metodi Ruby:

Si installa con:

gem install kumi

Richiede Ruby 3.1 o successivo ed è sotto licenza MIT. La versione attuale è la 0.1.0 del giugno 2026, e il README descrive il progetto come sperimentale: l’API pubblica può ancora cambiare.

Un esempio: una fattura svizzera

Per provarlo ho scritto una piccola fattura. Gli articoli con quantità pari o superiore a 10 ricevono uno sconto quantità del 10%, si aggiunge l’IVA svizzera dell’8,1% e il totale viene arrotondato ai 5 centesimi, come d’uso per gli importi in contanti in Svizzera.

require "kumi"

module Invoice
  extend Kumi::Schema

  schema do
    input do
      array :items do
        hash :item do
          integer :quantity
          decimal :unit_price
        end
      end
    end

    trait :bulk, input.items.item.quantity >= 10

    let :gross, input.items.item.quantity * input.items.item.unit_price
    value :line_totals, select(bulk, gross * 0.9, gross)
    value :subtotal, fn(:sum, line_totals)
    value :vat, subtotal * 0.081
    # no round() yet: scale to 5 Rappen, add 0.5, truncate
    value :total, fn(:to_integer, (subtotal + vat) * 20 + 0.5) / 20.0
  end
end

result = Invoice.from(items: [
  { quantity: 2,  unit_price: 50.0 },
  { quantity: 12, unit_price: 10.0 }
])

result[:line_totals] # => [100.0, 108.0]
result[:subtotal]    # => 208.0
result[:vat]         # => 16.848
result[:total]       # => 224.85

Alcune cose da notare:

La stessa logica in JavaScript

Questa è la parte che più mi interessa per i webshop. Una sola chiamata scrive un modulo JavaScript:

Invoice.write_source("invoice.mjs", platform: :javascript)

Ogni value, let e trait diventa una funzione esportata. Questa è la _total generata, senza modifiche:

export function _total(input) {
  let t88 = input["items"];
  let acc95 = 0;
  for (let items_i90 = 0; items_i90 < t88.length; items_i90++) {
    let items_el89 = t88[items_i90];
    let t91 = items_el89["quantity"];
    let t92 = 10;
    let t41 = t91 >= t92;
    let t93 = items_el89["unit_price"];
    let t48 = t91 * t93;
    let t94 = 0.9;
    let t51 = t48 * t94;
    let t59 = t41 ? t51 : t48;
    acc95 += t59;
  }
  let t96 = 0.081;
  let t87 = acc95 * t96;
  let t28 = acc95 + t87;
  let t97 = 20;
  let t30 = t28 * t97;
  let t98 = 0.5;
  let t32 = t30 + t98;
  let t33 = __core_to_integer(t32);
  let t99 = 20.0;
  let t35 = t33 / t99;
  return t35;
}

È esattamente ciò che voglio dal codice generato: un unico ciclo che calcola sconto, somma, IVA e arrotondamento, senza librerie né interpreti dietro. Potete leggerlo, seguirlo passo passo nel debugger e distribuirlo così com’è. Ho eseguito il modulo con Node.js sullo stesso input e ho ottenuto esattamente gli stessi risultati di Ruby: [100, 108], 208, 16.848 e 224.85.

Kumi in ruby.wasm

Lavoro molto con ruby.wasm, per esempio per i Ruby Koans nel browser, quindi volevo sapere se anche Kumi stesso gira dentro Ruby compilato in WebAssembly. Sì, con poche righe di configurazione:

# before require "kumi"
ENV["KUMI_PASS_BUDGET_MS"] = "0"   # no threads in WebAssembly: no compile time limit
require "tmpdir"
Dir.mkdir("/tmp") unless Dir.exist?("/tmp")
def Dir.tmpdir = "/tmp"            # a place for Kumi's code cache
require "kumi"

Kumi protegge ogni passo del compilatore con un limite di tempo basato su Timeout, che avvia un thread, e ruby.wasm non ha thread. La variabile d’ambiente disattiva il limite. Kumi inoltre scrive il codice Ruby generato in una directory di cache prima di caricarlo, e nel file system in memoria del browser Ruby non trova da solo una directory temporanea.

Con questa configurazione, l’esempio di fattura qui sopra dà esattamente gli stessi risultati in ruby.wasm, fino al 224.85. L’ho testato con ruby.wasm 2.10.1 (Ruby 4.0) in Node.js, usando lo stesso file system in memoria che ruby.wasm usa nel browser. Kumi è puro Ruby, e le sue dipendenze zeitwerk e mutex_m si caricano senza alcuna modifica.

Avete quindi due modi per portare le vostre regole nel browser: esportarle in JavaScript, l’opzione più leggera, oppure, se il vostro frontend esegue già Ruby in WebAssembly, definire ed eseguire gli schemi Kumi direttamente lì.

Gli errori vengono individuati presto

Kumi analizza le condizioni alla definizione dello schema. Se dichiarate che un’età è compresa tra 0 e 150 e poi scrivete una condizione che non può mai essere vera, il caricamento dello schema fallisce:

schema do
  input { integer :age, domain: 0..150 }
  trait :impossible_age, input.age == 200
end
# Kumi::Core::Errors::SemanticError: conjunction `impossible_age` is impossible

Nelle regole di business, una condizione del genere è di solito un refuso o un malinteso sui requisiti. Kumi ve lo dice nel momento in cui il codice viene caricato, invece di farvi scoprire mesi dopo che uno sconto non è mai stato applicato. Già solo questo vale uno sguardo.

Comporre le regole

Gli schemi possono importare altri schemi con import :tax, from: TaxPolicy. Una regola che conosce un solo importo può così essere applicata a ogni articolo di un ordine, e il compilatore la integra inline nel codice generato. La documentazione sugli import di schemi mostra come funziona.

Buono a sapersi

Provatelo nel browser

Il modo più rapido per farsi un’idea di Kumi è il playground interattivo. Contiene un’introduzione al linguaggio ed esempi più ampi, come un calcolatore delle imposte USA e un calcolo dei salari su 10’000 dipendenti.

Kumi è perfetto ovunque lo stesso calcolo debba girare sul server e nel browser: calcolatori di prezzo, configuratori di offerte, spese di spedizione. Uno schema, due linguaggi, risultati identici. Non vedo l’ora di usarlo nei miei progetti e di vedere dove andrà.