ESC
Type to search...
S
Soli Docs

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 checkwarning: 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.)

Braces or end. enum Name { ... } and enum Name ... end are both accepted; soli fmt normalizes to the end form.
Variant names are written in PascalCase, like classes.
Trailing commas between variants are optional.