Enums
A type-safe set of named variants — some carrying data — matched exhaustively with pattern matching. Use them instead of stringly-typed states like "pending".
Declaring an enum
Each variant is either a plain unit variant or one that carries a payload of named fields (the field type is optional and used by soli check).
enum Status
Active,
Archived,
Pending(reason: String)
end
Constructing values
Reference a unit variant by name. Construct a payload variant by calling it — with positional or named arguments.
a = Status.Active # a unit variant
p = Status.Pending(reason: "kyc") # named argument
p = Status.Pending("kyc") # positional — same value
p.variant() # "Pending" — the variant tag as a String
Matching variants
Match on the variant with EnumName.Variant. A payload variant binds its fields positionally, in declaration order.
label = match status
Status.Active => "Live",
Status.Archived => "Archived",
Status.Pending(r) => "Waiting: " + r, # binds the `reason` field to `r`
end
# A multi-field payload binds left-to-right:
enum Shape
Circle(radius: Float),
Rect(w: Float, h: Float)
end
area = match shape
Shape.Circle(radius) => 3.14159 * radius * radius,
Shape.Rect(w, h) => w * h,
end
Methods on enums
An enum can carry behaviour. Define methods in the body; inside them, self is the value and match self dispatches on its variant.
enum Status
Active,
Archived,
Pending(reason: String)
def label -> String
match self
Status.Active => "Live",
Status.Pending(r) => "Waiting: " + r,
_ => "Archived",
end
end
end
Status.Pending(reason: "kyc").label() # "Waiting: kyc"
Equality & introspection
Enum values compare structurally: two values are equal when they are the same variant with equal payloads. variant() returns the variant name as a String — handy for serializing to a database column or JSON.
Status.Active == Status.Active # true
Status.Active == Status.Archived # false
Status.Pending(reason: "x") == Status.Pending(reason: "x") # true (structural)
Status.Pending(reason: "x") == Status.Pending(reason: "y") # false
Status.Pending(reason: "x").variant() # "Pending"
Exhaustiveness checking
When the value's type is a known enum, soli check warns if a match misses a variant and has no _ catch-all. It's a non-blocking warning — your code still runs.
def describe(s: Status) -> String
match s
Status.Active => "live",
Status.Pending(r) => "waiting: " + r,
end # missing `Archived`, no `_`
end
soli check → warning: match on enum 'Status' is not exhaustive — missing: Archived (add them, or a `_ =>` arm)
Persisting enums in models
Declare enum_field :name, EnumType on a model and the column round-trips automatically: a unit variant is stored as its tag string, a payload variant as a tagged object, and reads rebuild the enum value. Define the enum above the model so it's loaded first.
enum Status
Pending(reason: String),
Paid,
Shipped,
Cancelled(reason: String)
end
class Order < Model
enum_field :status, Status
def can_ship -> Bool
match this.status
Status.Paid => true,
_ => false,
end
end
end
Now a real flow — create, read, transition, and branch on the status:
def create(req)
# The enum value is stored as its tag / tagged object automatically.
order = Order.create({ status: Status.Pending(reason: "awaiting payment") })
redirect("/orders/" + order._key)
end
def pay(req)
order = Order.find(req["params"]["id"])
order.status = Status.Paid # stored as "Paid"
order.save()
render_json({ "can_ship": order.can_ship() }) # true
end
def show(req)
order = Order.find(req["params"]["id"])
# `order.status` comes back as a Status value, not a raw string:
label = match order.status
Status.Pending(r) => "Pending: " + r,
Status.Paid => "Paid",
Status.Shipped => "Shipped",
Status.Cancelled(r) => "Cancelled: " + r,
end
render("orders/show", { "order": order, "label": label })
end
What lands in the database column:
# unit variant → a plain string
"Paid"
# payload variant → a tagged object
{ "variant": "Pending", "reason": "awaiting payment" }
Need to rebuild an enum from a stored value by hand (e.g. from a webhook payload)? Use Status.parse(value) — it accepts either form. (The factory is parse, not from, because from is a reserved word.)
end. enum Name { ... } and enum Name ... end are both accepted; soli fmt normalizes to the end form.PascalCase, like classes.