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.