SCUA

How-to

Validate and transform incoming data

Most of these guides use game examples, but the type and error tools that model a game entity work just as well on plain data: incoming orders, form submissions, records from a queue. This walkthrough takes a batch of loose order data, validates each item against rules, and totals the good ones, collecting the rejects instead of crashing on the first bad value.

The full program is examples/orders.scua; run it with scua examples/orders.scua.

#Describe the shape and the rules

A contract is a named rule. A record is a typed shape whose fields can carry those rules as where refinements:

contract Positive(n) n > 0 end
contract NonEmpty(s) s.len() > 0 end

record LineItem {
  sku:        string where NonEmpty,
  qty:        int where Positive,
  unit_price: decimal where Positive,
}

Now LineItem isn't just a shape, it's a shape with guarantees: a value can only become a LineItem if sku is non-empty and qty and unit_price are positive. The price is a decimal — exact base-10, never a float — so totals stay to the penny.

#Turn loose data into a trusted value

When loose data (something typed any, like a parsed JSON payload) is assigned to a typed binding, SCUA checks it once, at that line. A field that's the wrong type or breaks a where rule is rejected right there. This is "parse, don't validate": do the check at the edge, and the rest of your code can trust the value.

A rejected check raises a fault, not an Error value, so wrap the crossing in try and hand the caller a Result they can match on:

fn parse_item(raw)
  try
    let item: LineItem = raw   -- shape + the `where` rules run here
    return Ok(item)
  rescue e
    return Error(`{e.message}`)
  end
end

The fault that a refinement raises is introspectable: e.field, e.value, e.record, and e.message tell you exactly what failed, which is what you want when you're turning a rejection into an API response or a log line.

#Process a batch, keeping the rejects

With a fallible parse_item in hand, processing a batch is a loop that matches each result. A bad item is recorded and skipped; the good ones are totalled. The total is an exact decimal (never a float), and the integer quantity widens into it cleanly; money.format renders it with a currency symbol:

import money

fn process(order_id, raw_items)
  let total = 0.00d
  let rejected = 0
  for raw in raw_items do
    match parse_item(raw)
      Ok(item) -> total = total + item.qty * item.unit_price
      Error(why) -> do
        print(`  reject: {why}`)
        rejected = rejected + 1
      end
    end
  end
  print(`{order_id}: total {money.format(total, "USD")}, {rejected} rejected`)
end

Running it over one clean order and one with a bad quantity:

$ scua orders.scua
A-1001: total $27.96, 0 rejected
  reject: field 'qty' of LineItem violates its `where` constraint (got -5)
A-1002: total $50.00, 1 rejected

The shape, the rules, the boundary check, and the failures-as-values error model are the same pieces used everywhere else in SCUA. Nothing here is data-specific; it's the general-purpose core doing general-purpose work.

#One thing to know about depth

The boundary check is shallow: it confirms the top level of the value and runs that level's refinements, but it does not recurse into every nested record or array element. That's why this example validates each LineItem individually (binding each one to the typed record) rather than relying on a single check of a whole Order to validate its items. When you need a nested value guaranteed, parse it at its own level, as parse_item does here.