Skip to content

Examples

Real-world patterns where typestates prevent expensive bugs.

Running the Examples

All examples are complete, runnable files in the examples/ directory.

nim c -r examples/payment_processing.nim

Payment Processing

Payment processing requires strict ordering: authorize before capture, capture before refund. Typestates prevent costly mistakes like double-capture or refunds before capture.

Bugs prevented: Capturing before authorization, double-capture, refunding before capture, operations on settled payments.

## Payment Processing with Typestates
##
## The payment processing flow is a perfect typestate example because mistakes
## are EXPENSIVE. Charge before authorization? Chargeback. Refund twice?
## Money gone. Capture an expired authorization? Failed transaction.
##
## This example models the standard payment flow:
##   Created -> Authorized -> Captured -> (Refunded | Settled)
##
## The typestate ensures you CANNOT:
## - Capture without authorizing first
## - Refund before capturing
## - Capture an already-captured payment
## - Authorize an already-authorized payment

import ../src/typestates

type
  Payment = object
    id: string
    amount: int           # cents, to avoid float issues
    currency: string
    cardToken: string
    authCode: string
    capturedAt: int64
    refundedAmount: int

  # States represent where in the lifecycle this payment is
  Created = distinct Payment      ## Just created, not yet authorized
  Authorized = distinct Payment   ## Card charged, funds held, not yet captured
  Captured = distinct Payment     ## Funds transferred to merchant
  PartiallyRefunded = distinct Payment  ## Some amount refunded
  FullyRefunded = distinct Payment      ## Entire amount refunded
  Settled = distinct Payment      ## Batch settled, funds in bank
  Voided = distinct Payment       ## Authorization cancelled before capture

typestate Payment:
  # Payments may need status checks and can be refunded after completion.
  consumeOnTransition = false
  states Created, Authorized, Captured, PartiallyRefunded, FullyRefunded, Settled, Voided
  transitions:
    Created -> Authorized
    Authorized -> (Captured | Voided) as AuthResult
    Captured -> (PartiallyRefunded | FullyRefunded | Settled) as CaptureResult
    PartiallyRefunded -> (PartiallyRefunded | FullyRefunded | Settled) as RefundResult
    FullyRefunded -> Settled

# ============================================================================
# Transition procedures - each one enforces the state machine
# ============================================================================

proc authorize(p: Created, cardToken: string): Authorized {.transition.} =
  ## Authorize payment against the card.
  ## This places a hold on the customer's funds but doesn't transfer them.
  ## In production, this would call your payment processor API.
  var payment = p.Payment
  payment.cardToken = cardToken
  payment.authCode = "AUTH_" & payment.id
  echo "  [GATEWAY] Authorized $", payment.amount, " on card ending in ****"
  result = Authorized(payment)

proc capture(p: Authorized): Captured {.transition.} =
  ## Capture the authorized funds - money moves to merchant.
  ## Must happen within auth window (usually 7 days).
  var payment = p.Payment
  payment.capturedAt = 1234567890  # In real code: current timestamp
  echo "  [GATEWAY] Captured $", payment.amount, " (auth: ", payment.authCode, ")"
  result = Captured(payment)

proc void(p: Authorized): Voided {.transition.} =
  ## Cancel the authorization before capture.
  ## Releases the hold on customer's card - no money moved.
  echo "  [GATEWAY] Voided authorization ", p.Payment.authCode
  result = Voided(p.Payment)

proc partialRefund(p: Captured, amount: int): PartiallyRefunded {.transition.} =
  ## Refund part of the captured amount.
  var payment = p.Payment
  payment.refundedAmount = amount
  echo "  [GATEWAY] Partial refund: $", amount, " of $", payment.amount
  result = PartiallyRefunded(payment)

proc fullRefund(p: Captured): FullyRefunded {.transition.} =
  ## Refund the entire captured amount.
  var payment = p.Payment
  payment.refundedAmount = payment.amount
  echo "  [GATEWAY] Full refund: $", payment.amount
  result = FullyRefunded(payment)

proc additionalRefund(p: PartiallyRefunded, amount: int): RefundResult {.transition.} =
  ## Add more refund to a partially refunded payment.
  var payment = p.Payment
  payment.refundedAmount += amount
  echo "  [GATEWAY] Additional refund: $", amount
  if payment.refundedAmount >= payment.amount:
    result = RefundResult -> FullyRefunded(payment)
  else:
    result = RefundResult -> PartiallyRefunded(payment)

proc settle(p: Captured): Settled {.transition.} =
  ## Batch settlement - funds deposited to merchant bank.
  echo "  [GATEWAY] Settled $", p.Payment.amount, " to merchant account"
  result = Settled(p.Payment)

proc settle(p: PartiallyRefunded): Settled {.transition.} =
  ## Settle a partially refunded payment (net amount).
  let net = p.Payment.amount - p.Payment.refundedAmount
  echo "  [GATEWAY] Settled $", net, " (after refunds) to merchant account"
  result = Settled(p.Payment)

proc settle(p: FullyRefunded): Settled {.transition.} =
  ## Settle a fully refunded payment ($0 net).
  echo "  [GATEWAY] Settled $0 (fully refunded) - closing payment"
  result = Settled(p.Payment)

# ============================================================================
# Non-transition operations - query state without changing it
# ============================================================================

func amount(p: PaymentStates): int =
  ## Get the payment amount in cents.
  p.Payment.amount

func refundedAmount(p: PartiallyRefunded): int =
  ## How much has been refunded so far?
  p.Payment.refundedAmount

func remainingAmount(p: PartiallyRefunded): int =
  ## How much can still be refunded?
  p.Payment.amount - p.Payment.refundedAmount

# ============================================================================
# Example usage showing compile-time safety
# ============================================================================

when isMainModule:
  echo "=== Payment Processing Demo ===\n"

  echo "1. Creating payment for $99.99..."
  var payment = Created(Payment(
    id: "pay_abc123",
    amount: 9999,  # $99.99 in cents
    currency: "USD"
  ))

  echo "\n2. Authorizing payment..."
  let authorized = payment.authorize("card_tok_visa_4242")

  echo "\n3. Capturing funds..."
  let captured = authorized.capture()

  echo "\n4. Customer requests $25 refund..."
  let refunded = captured.partialRefund(2500)
  echo "   Remaining: $", refunded.remainingAmount()

  echo "\n5. End of day settlement..."
  let settled = refunded.settle()

  echo "\n=== Payment lifecycle complete! ===\n"

  # =========================================================================
  # COMPILE-TIME ERRORS - These are the bugs the typestate PREVENTS:
  # =========================================================================

  echo "The following bugs are caught at COMPILE TIME:\n"

  # BUG 1: Capturing without authorization
  # "Oops, we forgot to auth - charged the wrong amount!"
  # let oops1 = payment.capture()
  echo "  [PREVENTED] capture() on Created payment"

  # BUG 2: Double-capture
  # "Customer charged twice!"
  # let oops2 = captured.capture()
  echo "  [PREVENTED] capture() on already-Captured payment"

  # BUG 3: Refunding before capture
  # "Refunded money we never had!"
  # let oops3 = authorized.partialRefund(1000)
  echo "  [PREVENTED] partialRefund() on Authorized payment"

  # BUG 4: Refunding a settled payment
  # "Accounting nightmare - refund after books closed!"
  # let oops4 = settled.partialRefund(500)
  echo "  [PREVENTED] partialRefund() on Settled payment"

  # BUG 5: Voiding after capture
  # "Tried to void but money already moved!"
  # let oops5 = captured.void()
  echo "  [PREVENTED] void() on Captured payment"

  echo "\nUncomment any of the 'oops' lines above to see the compile error!"

View full source


Database Connection Pool

Connection pools have invariants that are easy to violate: don't query pooled connections, don't return connections mid-transaction, don't commit without a transaction.

Bugs prevented: Query on pooled connection, returning connection while in transaction, committing without transaction, nested transactions.

## Database Connection Pool with Typestates
##
## Connection pool bugs are among the most painful to debug:
## - Query on a closed connection: "connection already closed"
## - Return connection to pool while query is running: data corruption
## - Use connection after returning to pool: race conditions
## - Forget to return connection: pool exhaustion
##
## This example ensures compile-time safety for connection lifecycle.

import ../src/typestates

type
  DbConnection = object
    id: int
    host: string
    port: int
    database: string
    inTransaction: bool
    queryCount: int

  # Connection states
  Pooled = distinct DbConnection       ## In the pool, available for checkout
  CheckedOut = distinct DbConnection   ## Checked out, not in transaction
  InTransaction = distinct DbConnection ## Active transaction
  Closed = distinct DbConnection        ## Permanently closed

typestate DbConnection:
  # Connections are pooled and reused, so we disable ownership enforcement.
  # This allows checkout -> use -> release -> checkout cycles.
  consumeOnTransition = false
  states Pooled, CheckedOut, InTransaction, Closed
  transitions:
    Pooled -> (CheckedOut | Closed) as CheckoutResult       # Checkout or close idle connection
    CheckedOut -> (Pooled | InTransaction | Closed) as CheckoutAction  # Return, begin tx, or close
    InTransaction -> CheckedOut         # Commit/rollback ends transaction
    * -> Closed                         # Can always force-close

# ============================================================================
# Connection Pool Operations
# ============================================================================

proc checkout(conn: Pooled): CheckedOut {.transition.} =
  ## Get a connection from the pool for exclusive use.
  echo "  [POOL] Checked out connection #", conn.DbConnection.id
  result = CheckedOut(conn.DbConnection)

proc release(conn: CheckedOut): Pooled {.transition.} =
  ## Return connection to the pool.
  echo "  [POOL] Released connection #", conn.DbConnection.id, " (", conn.DbConnection.queryCount, " queries)"
  var c = conn.DbConnection
  c.queryCount = 0
  result = Pooled(c)

proc close(conn: Pooled): Closed {.transition.} =
  ## Close an idle connection permanently.
  echo "  [POOL] Closed idle connection #", conn.DbConnection.id
  result = Closed(conn.DbConnection)

proc close(conn: CheckedOut): Closed {.transition.} =
  ## Close a checked-out connection (emergency/error case).
  echo "  [POOL] Force-closed connection #", conn.DbConnection.id
  result = Closed(conn.DbConnection)

# ============================================================================
# Transaction Operations
# ============================================================================

proc beginTransaction(conn: CheckedOut): InTransaction {.transition.} =
  ## Start a database transaction.
  echo "  [DB] BEGIN TRANSACTION"
  var c = conn.DbConnection
  c.inTransaction = true
  result = InTransaction(c)

proc commit(conn: InTransaction): CheckedOut {.transition.} =
  ## Commit the current transaction.
  echo "  [DB] COMMIT"
  var c = conn.DbConnection
  c.inTransaction = false
  result = CheckedOut(c)

proc rollback(conn: InTransaction): CheckedOut {.transition.} =
  ## Rollback the current transaction.
  echo "  [DB] ROLLBACK"
  var c = conn.DbConnection
  c.inTransaction = false
  result = CheckedOut(c)

# ============================================================================
# Query Operations (no state change)
# ============================================================================

proc execute(conn: CheckedOut, sql: string): CheckedOut {.notATransition.} =
  ## Execute a SQL statement (outside transaction).
  var c = conn.DbConnection
  c.queryCount += 1
  echo "  [DB] Execute: ", sql
  result = CheckedOut(c)

proc execute(conn: InTransaction, sql: string): InTransaction {.notATransition.} =
  ## Execute a SQL statement (inside transaction).
  var c = conn.DbConnection
  c.queryCount += 1
  echo "  [DB] Execute (in tx): ", sql
  result = InTransaction(c)

func isInTransaction(conn: DbConnectionStates): bool =
  ## Check if connection has active transaction.
  conn.DbConnection.inTransaction

# ============================================================================
# Example Usage
# ============================================================================

when isMainModule:
  echo "=== Database Connection Demo ===\n"

  # Simulate a connection pool
  var pooledConn = Pooled(DbConnection(
    id: 42,
    host: "localhost",
    port: 5432,
    database: "myapp"
  ))

  echo "1. Checkout connection from pool..."
  let conn = pooledConn.checkout()

  echo "\n2. Execute some queries..."
  let conn2 = conn.execute("SELECT * FROM users WHERE id = 1")
  let conn3 = conn2.execute("UPDATE users SET last_login = NOW() WHERE id = 1")

  echo "\n3. Start a transaction for batch insert..."
  let tx = conn3.beginTransaction()

  echo "\n4. Execute transactional queries..."
  let tx2 = tx.execute("INSERT INTO audit_log VALUES (1, 'login', NOW())")
  let tx3 = tx2.execute("INSERT INTO sessions VALUES (1, 'abc123', NOW())")

  echo "\n5. Commit the transaction..."
  let afterTx = tx3.commit()

  echo "\n6. Return connection to pool..."
  let returned = afterTx.release()

  echo "\n=== Connection lifecycle complete! ===\n"

  # =========================================================================
  # COMPILE-TIME ERRORS - These bugs are prevented:
  # =========================================================================

  echo "The following bugs are caught at COMPILE TIME:\n"

  # BUG 1: Query on pooled (not checked out) connection
  # let bad1 = returned.execute("SELECT 1")
  echo "  [PREVENTED] execute() on Pooled connection"

  # BUG 2: Return connection while in transaction
  # let bad2 = tx.release()
  echo "  [PREVENTED] release() on InTransaction connection"

  # BUG 3: Commit without starting transaction
  # let bad3 = conn.commit()
  echo "  [PREVENTED] commit() on CheckedOut connection (no transaction)"

  # BUG 4: Double checkout (already checked out)
  # let bad4 = conn.checkout()
  echo "  [PREVENTED] checkout() on CheckedOut connection"

  # BUG 5: Begin transaction inside transaction
  # let bad5 = tx.beginTransaction()
  echo "  [PREVENTED] beginTransaction() on InTransaction connection"

  echo "\nUncomment any of the 'bad' lines above to see the compile error!"

View full source


HTTP Request Lifecycle

HTTP requests follow a strict sequence: set headers, send headers, send body, await response. Typestates enforce this ordering at compile time.

Bugs prevented: Adding headers after sent, sending body before headers, reading response before request complete.

## HTTP Request Lifecycle with Typestates
##
## HTTP requests have a strict lifecycle that's easy to mess up:
## - Writing body after sending headers
## - Reading response before sending request
## - Sending headers twice
## - Using a connection after it's closed
##
## This example models the full request/response lifecycle.

import ../src/typestates

type
  HttpRequest = object
    meth: string
    path: string
    headers: seq[(string, string)]
    body: string
    responseCode: int
    responseBody: string

  # Request states
  Building = distinct HttpRequest      ## Accumulating headers
  HeadersSent = distinct HttpRequest   ## Headers sent, can send body
  RequestSent = distinct HttpRequest   ## Full request sent, awaiting response
  ResponseReceived = distinct HttpRequest  ## Response received, can read
  Closed = distinct HttpRequest        ## Connection closed

typestate HttpRequest:
  # HTTP connections support keep-alive (reuse after response).
  consumeOnTransition = false
  states Building, HeadersSent, RequestSent, ResponseReceived, Closed
  transitions:
    Building -> HeadersSent
    HeadersSent -> RequestSent         # Send body/finalize request
    RequestSent -> ResponseReceived    # Receive response
    ResponseReceived -> (Closed | Building) as ResponseAction  # Close or reuse for keep-alive
    * -> Closed                        # Can always abort

# ============================================================================
# Building the request
# ============================================================================

proc newRequest(meth: string, path: string): Building =
  ## Create a new HTTP request builder.
  echo "  [HTTP] ", meth, " ", path
  result = Building(HttpRequest(meth: meth, path: path))

proc header(req: Building, key: string, value: string): Building {.notATransition.} =
  ## Add a header to the request.
  var r = req.HttpRequest
  r.headers.add((key, value))
  echo "  [HTTP] Header: ", key, ": ", value
  result = Building(r)

proc sendHeaders(req: Building): HeadersSent {.transition.} =
  ## Finalize and send the headers.
  echo "  [HTTP] >>> Sending headers..."
  result = HeadersSent(req.HttpRequest)

# ============================================================================
# Sending the request
# ============================================================================

proc sendBody(req: HeadersSent, body: string): RequestSent {.transition.} =
  ## Send the request body (for POST, PUT, etc.).
  var r = req.HttpRequest
  r.body = body
  echo "  [HTTP] >>> Sending body (", body.len, " bytes)"
  result = RequestSent(r)

proc finish(req: HeadersSent): RequestSent {.transition.} =
  ## Finish request without body (for GET, DELETE, etc.).
  echo "  [HTTP] >>> Request complete (no body)"
  result = RequestSent(req.HttpRequest)

# ============================================================================
# Receiving the response
# ============================================================================

proc awaitResponse(req: RequestSent): ResponseReceived {.transition.} =
  ## Wait for and receive the response.
  var r = req.HttpRequest
  # Simulate response
  r.responseCode = 200
  r.responseBody = """{"status": "ok", "data": [1, 2, 3]}"""
  echo "  [HTTP] <<< Response: ", r.responseCode
  result = ResponseReceived(r)

func statusCode(resp: ResponseReceived): int =
  ## Get the HTTP status code.
  resp.HttpRequest.responseCode

func body(resp: ResponseReceived): string =
  ## Get the response body.
  resp.HttpRequest.responseBody

func isSuccess(resp: ResponseReceived): bool =
  ## Check if response indicates success (2xx).
  let code = resp.HttpRequest.responseCode
  code >= 200 and code < 300

# ============================================================================
# Closing or reusing
# ============================================================================

proc close(resp: ResponseReceived): Closed {.transition.} =
  ## Close the connection.
  echo "  [HTTP] Connection closed"
  result = Closed(resp.HttpRequest)

proc reuse(resp: ResponseReceived): Building {.transition.} =
  ## Reuse connection for another request (keep-alive).
  echo "  [HTTP] Reusing connection (keep-alive)"
  var r = HttpRequest()  # Fresh request on same connection
  result = Building(r)

# ============================================================================
# Example Usage
# ============================================================================

when isMainModule:
  echo "=== HTTP Request Demo ===\n"

  echo "1. Building GET request..."
  let req1 = newRequest("GET", "/api/users")
    .header("Accept", "application/json")
    .header("Authorization", "Bearer token123")

  echo "\n2. Sending headers..."
  let headersSent = req1.sendHeaders()

  echo "\n3. Finishing request (no body for GET)..."
  let sent = headersSent.finish()

  echo "\n4. Awaiting response..."
  let response = sent.awaitResponse()

  echo "\n5. Reading response..."
  echo "   Status: ", response.statusCode()
  echo "   Body: ", response.body()
  echo "   Success: ", response.isSuccess()

  echo "\n6. Closing connection..."
  let closed = response.close()

  echo "\n=== Request lifecycle complete! ===\n"

  # =========================================================================
  # COMPILE-TIME ERRORS - These bugs are prevented:
  # =========================================================================

  echo "The following bugs are caught at COMPILE TIME:\n"

  # BUG 1: Adding headers after they're sent
  # let bad1 = headersSent.header("X-Late", "header")
  echo "  [PREVENTED] header() after sendHeaders()"

  # BUG 2: Sending body on GET (before sending headers)
  # let bad2 = req1.sendBody("data")
  echo "  [PREVENTED] sendBody() before sendHeaders()"

  # BUG 3: Reading response before request is sent
  # let bad3 = headersSent.statusCode()
  echo "  [PREVENTED] statusCode() before response received"

  # BUG 4: Sending more data after request is complete
  # let bad4 = sent.sendBody("more data")
  echo "  [PREVENTED] sendBody() after request sent"

  # BUG 5: Using closed connection
  # let bad5 = closed.reuse()
  echo "  [PREVENTED] reuse() on Closed connection"

  echo "\nUncomment any of the 'bad' lines above to see the compile error!"

View full source


OAuth Authentication

OAuth requires authenticated tokens for API calls and refresh tokens to renew expired access. Typestates prevent calls with missing or expired credentials.

Bugs prevented: API calls without authentication, API calls with expired token, refreshing non-expired token.

## OAuth 2.0 Authentication Flow with Typestates
##
## OAuth flows are notoriously easy to get wrong:
## - Using an expired token
## - Calling API before authenticating
## - Refreshing with an invalid refresh token
## - Skipping PKCE verification
##
## This example models the Authorization Code + PKCE flow.

import ../src/typestates

type
  OAuthSession = object
    clientId: string
    redirectUri: string
    codeVerifier: string     # PKCE
    codeChallenge: string    # PKCE
    authCode: string
    accessToken: string
    refreshToken: string
    expiresAt: int64

  # OAuth states
  Unauthenticated = distinct OAuthSession  ## No tokens yet
  AwaitingCallback = distinct OAuthSession ## Auth URL generated, waiting for callback
  Authenticated = distinct OAuthSession    ## Have valid access token
  TokenExpired = distinct OAuthSession     ## Access token expired, need refresh
  RefreshFailed = distinct OAuthSession    ## Refresh failed, need re-auth

typestate OAuthSession:
  # OAuth tokens may be read multiple times and refreshed.
  consumeOnTransition = false
  states Unauthenticated, AwaitingCallback, Authenticated, TokenExpired, RefreshFailed
  transitions:
    Unauthenticated -> AwaitingCallback    # Start auth flow
    AwaitingCallback -> Authenticated      # Callback received, tokens exchanged
    Authenticated -> TokenExpired          # Token expired
    TokenExpired -> Authenticated          # Refresh succeeded
    TokenExpired -> RefreshFailed          # Refresh failed
    RefreshFailed -> AwaitingCallback      # Start over
    * -> Unauthenticated                   # Logout

# ============================================================================
# Starting the flow
# ============================================================================

proc startAuth(session: Unauthenticated, clientId: string, redirectUri: string): AwaitingCallback {.transition.} =
  ## Generate authorization URL and PKCE challenge.
  var s = session.OAuthSession
  s.clientId = clientId
  s.redirectUri = redirectUri
  s.codeVerifier = "random_verifier_string_43_chars_min"  # In prod: secure random
  s.codeChallenge = "hashed_challenge"  # In prod: SHA256(verifier)

  let authUrl = "https://auth.example.com/authorize?" &
    "client_id=" & clientId &
    "&redirect_uri=" & redirectUri &
    "&code_challenge=" & s.codeChallenge &
    "&code_challenge_method=S256"

  echo "  [OAUTH] Authorization URL generated"
  echo "  [OAUTH] Redirect user to: ", authUrl[0..50], "..."
  result = AwaitingCallback(s)

# ============================================================================
# Handling the callback
# ============================================================================

proc handleCallback(session: AwaitingCallback, authCode: string): Authenticated {.transition.} =
  ## Exchange authorization code for tokens.
  var s = session.OAuthSession
  s.authCode = authCode

  # In production: POST to token endpoint with code + code_verifier
  echo "  [OAUTH] Exchanging auth code for tokens..."
  echo "  [OAUTH] Verifying PKCE: code_verifier=", s.codeVerifier[0..10], "..."

  s.accessToken = "eyJhbGc..." & authCode[0..5]
  s.refreshToken = "refresh_" & authCode[0..5]
  s.expiresAt = 1234567890 + 3600

  echo "  [OAUTH] Access token received (expires in 1h)"
  result = Authenticated(s)

# ============================================================================
# Using the API
# ============================================================================

proc callApi(session: Authenticated, endpoint: string): string {.notATransition.} =
  ## Make an authenticated API call.
  echo "  [API] GET ", endpoint
  echo "  [API] Authorization: Bearer ", session.OAuthSession.accessToken[0..10], "..."
  result = """{"user": "alice", "email": "alice@example.com"}"""

proc getAccessToken(session: Authenticated): string =
  ## Get the current access token for manual use.
  session.OAuthSession.accessToken

# ============================================================================
# Token expiration and refresh
# ============================================================================

proc tokenExpired(session: Authenticated): TokenExpired {.transition.} =
  ## Mark the access token as expired.
  echo "  [OAUTH] Access token expired!"
  result = TokenExpired(session.OAuthSession)

proc refresh(session: TokenExpired): Authenticated {.transition.} =
  ## Refresh the access token using the refresh token.
  var s = session.OAuthSession

  # In production: POST to token endpoint with refresh_token
  echo "  [OAUTH] Refreshing token using: ", s.refreshToken[0..10], "..."

  s.accessToken = "eyJhbGc...refreshed"
  s.expiresAt = 1234567890 + 7200

  echo "  [OAUTH] New access token received"
  result = Authenticated(s)

proc refreshFailed(session: TokenExpired): RefreshFailed {.transition.} =
  ## Handle refresh failure (e.g., refresh token revoked).
  echo "  [OAUTH] Refresh failed! Token may be revoked."
  result = RefreshFailed(session.OAuthSession)

proc restartAuth(session: RefreshFailed): AwaitingCallback {.transition.} =
  ## Start authentication flow again after refresh failure.
  var s = session.OAuthSession
  s.accessToken = ""
  s.refreshToken = ""
  s.codeVerifier = "new_verifier_for_retry"
  s.codeChallenge = "new_challenge"

  echo "  [OAUTH] Starting fresh authentication..."
  result = AwaitingCallback(s)

# ============================================================================
# Logout
# ============================================================================

proc logout(session: Authenticated): Unauthenticated {.transition.} =
  ## Log out and revoke tokens.
  echo "  [OAUTH] Logging out, revoking tokens..."
  result = Unauthenticated(OAuthSession())

# ============================================================================
# Example Usage
# ============================================================================

when isMainModule:
  echo "=== OAuth 2.0 Authentication Demo ===\n"

  echo "1. Creating unauthenticated session..."
  let session = Unauthenticated(OAuthSession())

  echo "\n2. Starting OAuth flow (PKCE)..."
  let awaiting = session.startAuth(
    clientId = "my-app-client-id",
    redirectUri = "myapp://callback"
  )

  echo "\n3. User authorizes, handling callback..."
  let authed = awaiting.handleCallback(authCode = "abc123xyz")

  echo "\n4. Making authenticated API calls..."
  let userData = authed.callApi("/api/user/me")
  echo "   Response: ", userData

  echo "\n5. Simulating token expiration..."
  let expired = authed.tokenExpired()

  echo "\n6. Refreshing access token..."
  let refreshed = expired.refresh()

  echo "\n7. Making another API call with new token..."
  let moreData = refreshed.callApi("/api/user/settings")

  echo "\n8. Logging out..."
  let loggedOut = refreshed.logout()

  echo "\n=== OAuth flow complete! ===\n"

  # =========================================================================
  # COMPILE-TIME ERRORS - These bugs are prevented:
  # =========================================================================

  echo "The following bugs are caught at COMPILE TIME:\n"

  # BUG 1: API call without authentication
  # let bad1 = session.callApi("/api/secret")
  echo "  [PREVENTED] callApi() on Unauthenticated session"

  # BUG 2: API call with expired token
  # let bad2 = expired.callApi("/api/data")
  echo "  [PREVENTED] callApi() on TokenExpired session"

  # BUG 3: Refresh without expiration
  # let bad3 = authed.refresh()
  echo "  [PREVENTED] refresh() on Authenticated session (not expired)"

  # BUG 4: Handle callback twice
  # let bad4 = authed.handleCallback("another_code")
  echo "  [PREVENTED] handleCallback() on Authenticated session"

  # BUG 5: Use logged out session
  # let bad5 = loggedOut.getAccessToken()
  echo "  [PREVENTED] getAccessToken() on Unauthenticated session"

  echo "\nUncomment any of the 'bad' lines above to see the compile error!"

View full source


Robot Arm Controller

Hardware control requires strict operation sequences. Moving without homing can crash into limits; powering off during movement can damage motors.

Bugs prevented: Moving without homing, power off while moving, continuing after emergency stop.

## Robot Arm Controller with Typestates
##
## Hardware control is where typestates REALLY shine. Wrong operation order
## can damage expensive equipment or cause safety hazards:
## - Moving arm before homing: crash into limits
## - Operating without calibration: inaccurate positioning
## - Emergency stop not handled: damage or injury
## - Power off while moving: motor damage
##
## This example models a robotic arm controller with safety states.

import ../src/typestates

type
  RobotArm = object
    x, y, z: float          # Current position
    homeX, homeY, homeZ: float  # Home position
    speed: float            # Movement speed
    toolAttached: bool
    emergencyReason: string

  # Robot arm states
  PoweredOff = distinct RobotArm     ## No power to motors
  Initializing = distinct RobotArm   ## Powering up, running diagnostics
  NeedsHoming = distinct RobotArm    ## Powered but position unknown
  Homing = distinct RobotArm         ## Currently finding home position
  Ready = distinct RobotArm          ## Homed and ready for commands
  Moving = distinct RobotArm         ## Currently executing movement
  EmergencyStop = distinct RobotArm  ## E-stop triggered, frozen

typestate RobotArm:
  # Robot arm state needs to be inspected for position, calibration, etc.
  consumeOnTransition = false
  states PoweredOff, Initializing, NeedsHoming, Homing, Ready, Moving, EmergencyStop
  transitions:
    PoweredOff -> Initializing
    Initializing -> NeedsHoming
    NeedsHoming -> Homing
    Homing -> Ready
    Ready -> (Moving | PoweredOff) as ReadyAction
    Moving -> (Ready | EmergencyStop) as MovementResult
    EmergencyStop -> (NeedsHoming | PoweredOff) as EmergencyAction  # Must re-home after E-stop

# ============================================================================
# Power and Initialization
# ============================================================================

proc powerOn(arm: PoweredOff): Initializing {.transition.} =
  ## Power on the robot arm and start initialization.
  echo "  [ARM] Powering on..."
  echo "  [ARM] Running motor diagnostics..."
  result = Initializing(arm.RobotArm)

proc completeInit(arm: Initializing): NeedsHoming {.transition.} =
  ## Complete initialization, now needs homing.
  echo "  [ARM] Diagnostics passed"
  echo "  [ARM] WARNING: Position unknown - homing required!"
  result = NeedsHoming(arm.RobotArm)

proc powerOff(arm: Ready): PoweredOff {.transition.} =
  ## Safely power off the arm when ready.
  echo "  [ARM] Powering off safely..."
  result = PoweredOff(arm.RobotArm)

proc powerOffEmergency(arm: EmergencyStop): PoweredOff {.transition.} =
  ## Power off after emergency stop.
  echo "  [ARM] Emergency power off"
  result = PoweredOff(arm.RobotArm)

# ============================================================================
# Homing Operations
# ============================================================================

proc startHoming(arm: NeedsHoming): Homing {.transition.} =
  ## Begin the homing sequence to find reference position.
  echo "  [ARM] Starting homing sequence..."
  echo "  [ARM] Moving to limit switches at low speed..."
  result = Homing(arm.RobotArm)

proc homingComplete(arm: Homing, homeX, homeY, homeZ: float): Ready {.transition.} =
  ## Complete homing and set reference position.
  var a = arm.RobotArm
  a.x = homeX
  a.y = homeY
  a.z = homeZ
  a.homeX = homeX
  a.homeY = homeY
  a.homeZ = homeZ
  echo "  [ARM] Homing complete. Position: (", homeX, ", ", homeY, ", ", homeZ, ")"
  echo "  [ARM] Ready for commands!"
  result = Ready(a)

proc resetAfterEmergency(arm: EmergencyStop): NeedsHoming {.transition.} =
  ## Reset emergency stop - position is now uncertain.
  echo "  [ARM] E-stop reset. Position uncertain - must re-home!"
  var a = arm.RobotArm
  a.emergencyReason = ""
  result = NeedsHoming(a)

# ============================================================================
# Movement Operations
# ============================================================================

proc moveTo(arm: Ready, x, y, z: float): Moving {.transition.} =
  ## Start moving to target position.
  echo "  [ARM] Moving to (", x, ", ", y, ", ", z, ")..."
  result = Moving(arm.RobotArm)

proc moveComplete(arm: Moving, x, y, z: float): Ready {.transition.} =
  ## Movement completed successfully.
  var a = arm.RobotArm
  a.x = x
  a.y = y
  a.z = z
  echo "  [ARM] Reached position (", x, ", ", y, ", ", z, ")"
  result = Ready(a)

proc emergencyStop(arm: Moving, reason: string): EmergencyStop {.transition.} =
  ## Trigger emergency stop during movement!
  var a = arm.RobotArm
  a.emergencyReason = reason
  echo "  [ARM] !!! EMERGENCY STOP !!!"
  echo "  [ARM] Reason: ", reason
  echo "  [ARM] Motors locked. Manual intervention required."
  result = EmergencyStop(a)

# ============================================================================
# Status and Configuration (no state change)
# ============================================================================

func position(arm: Ready): tuple[x, y, z: float] =
  ## Get current position (only valid when Ready).
  (arm.RobotArm.x, arm.RobotArm.y, arm.RobotArm.z)

proc setSpeed(arm: Ready, speed: float): Ready {.notATransition.} =
  ## Configure movement speed.
  var a = arm.RobotArm
  a.speed = speed
  echo "  [ARM] Speed set to ", speed, " mm/s"
  result = Ready(a)

proc attachTool(arm: Ready, toolName: string): Ready {.notATransition.} =
  ## Attach a tool to the arm.
  var a = arm.RobotArm
  a.toolAttached = true
  echo "  [ARM] Tool attached: ", toolName
  result = Ready(a)

# ============================================================================
# Example Usage
# ============================================================================

when isMainModule:
  echo "=== Robot Arm Controller Demo ===\n"

  echo "1. Starting with powered-off arm..."
  var arm = PoweredOff(RobotArm())

  echo "\n2. Powering on..."
  let initializing = arm.powerOn()

  echo "\n3. Completing initialization..."
  let needsHoming = initializing.completeInit()

  echo "\n4. Starting homing sequence..."
  let homing = needsHoming.startHoming()

  echo "\n5. Homing complete..."
  let ready = homing.homingComplete(0.0, 0.0, 100.0)

  echo "\n6. Configuring arm..."
  let configured = ready
    .setSpeed(50.0)
    .attachTool("gripper")

  echo "\n7. Moving to pick position..."
  let moving1 = configured.moveTo(100.0, 50.0, 20.0)
  let atPick = moving1.moveComplete(100.0, 50.0, 20.0)

  echo "\n8. Moving to place position..."
  let moving2 = atPick.moveTo(200.0, 50.0, 20.0)
  let atPlace = moving2.moveComplete(200.0, 50.0, 20.0)

  echo "\n9. Returning home and powering off..."
  let moving3 = atPlace.moveTo(0.0, 0.0, 100.0)
  let atHome = moving3.moveComplete(0.0, 0.0, 100.0)
  let off = atHome.powerOff()

  echo "\n=== Demo complete! ===\n"

  # =========================================================================
  # COMPILE-TIME ERRORS - These dangerous bugs are prevented:
  # =========================================================================

  echo "The following DANGEROUS bugs are caught at COMPILE TIME:\n"

  # BUG 1: Moving without homing (position unknown!)
  # let bad1 = needsHoming.moveTo(100.0, 0.0, 0.0)
  echo "  [PREVENTED] moveTo() without homing - could crash into limits!"

  # BUG 2: Power off while moving (motor damage!)
  # let bad2 = moving1.powerOff()
  echo "  [PREVENTED] powerOff() while moving - motor damage risk!"

  # BUG 3: Continue after emergency stop
  # let estop = moving2.emergencyStop("Obstacle detected")
  # let bad3 = estop.moveTo(0.0, 0.0, 0.0)
  echo "  [PREVENTED] moveTo() after E-stop - dangerous!"

  # BUG 4: Skip initialization
  # let bad4 = initializing.startHoming()
  echo "  [PREVENTED] startHoming() before initialization complete"

  # BUG 5: Configure speed while moving
  # let bad5 = moving1.setSpeed(100.0)
  echo "  [PREVENTED] setSpeed() while moving - could cause issues"

  echo "\nUncomment any of the 'bad' lines to see the compile error!"

View full source


Order Fulfillment

Order fulfillment has a fixed sequence: place, pay, ship, deliver. Typestates ensure orders can't be shipped before payment or shipped twice.

Bugs prevented: Ship before payment, double-ship, operations on unplaced cart.

## E-commerce Order Fulfillment with Typestates
##
## Order fulfillment bugs are expensive and embarrassing:
## - Shipping before payment: lost merchandise
## - Refunding unshipped orders: process confusion
## - Double-shipping: inventory nightmare
## - Cancelling already-shipped: customer confusion
##
## This example models the complete order lifecycle.

import ../src/typestates
import std/hashes

type
  Order = object
    id: string
    customerId: string
    items: seq[(string, int, int)]  # (sku, qty, price)
    total: int
    paymentId: string
    trackingNumber: string
    cancelReason: string

  # Order states
  Cart = distinct Order           ## Items being added, not yet placed
  Placed = distinct Order         ## Order submitted, pending payment
  Paid = distinct Order           ## Payment received
  Picking = distinct Order        ## Warehouse picking items
  Packed = distinct Order         ## Items packed, ready to ship
  Shipped = distinct Order        ## Handed to carrier
  Delivered = distinct Order      ## Customer received package
  Cancelled = distinct Order      ## Order cancelled
  Returned = distinct Order       ## Items returned by customer

typestate Order:
  # Orders need to be inspected at various stages (for status, totals, etc.).
  consumeOnTransition = false
  states Cart, Placed, Paid, Picking, Packed, Shipped, Delivered, Cancelled, Returned
  transitions:
    Cart -> Placed                     # Submit order
    Placed -> (Paid | Cancelled) as PaymentResult         # Pay or cancel
    Paid -> (Picking | Cancelled) as FulfillmentAction        # Start fulfillment or cancel (refund)
    Picking -> Packed                  # Finish picking
    Packed -> Shipped                  # Hand to carrier
    Shipped -> Delivered               # Delivery confirmed
    Delivered -> Returned              # Customer returns
    * -> Cancelled                     # Can always cancel (with appropriate handling)

# ============================================================================
# Cart Operations
# ============================================================================

proc newOrder(customerId: string): Cart =
  ## Create a new shopping cart.
  echo "  [ORDER] New cart for customer: ", customerId
  result = Cart(Order(customerId: customerId))

proc addItem(order: Cart, sku: string, qty: int, price: int): Cart {.notATransition.} =
  ## Add an item to the cart.
  var o = order.Order
  o.items.add((sku, qty, price))
  o.total += qty * price
  echo "  [ORDER] Added ", qty, "x ", sku, " @ $", price, " = $", qty * price
  result = Cart(o)

proc placeOrder(order: Cart): Placed {.transition.} =
  ## Submit the order for processing.
  var o = order.Order
  o.id = "ORD-" & $hash(o.customerId)  # Simplified ID generation
  echo "  [ORDER] Order placed: ", o.id, " (total: $", o.total, ")"
  result = Placed(o)

# ============================================================================
# Payment
# ============================================================================

proc pay(order: Placed, paymentId: string): Paid {.transition.} =
  ## Record payment for the order.
  var o = order.Order
  o.paymentId = paymentId
  echo "  [ORDER] Payment received: ", paymentId
  result = Paid(o)

proc cancelUnpaid(order: Placed, reason: string): Cancelled {.transition.} =
  ## Cancel an unpaid order (no refund needed).
  var o = order.Order
  o.cancelReason = reason
  echo "  [ORDER] Order cancelled (unpaid): ", reason
  result = Cancelled(o)

proc cancelWithRefund(order: Paid, reason: string): Cancelled {.transition.} =
  ## Cancel a paid order and process refund.
  var o = order.Order
  o.cancelReason = reason
  echo "  [ORDER] Order cancelled with refund"
  echo "  [ORDER] Refunding payment: ", o.paymentId
  result = Cancelled(o)

# ============================================================================
# Fulfillment
# ============================================================================

proc startPicking(order: Paid): Picking {.transition.} =
  ## Start warehouse picking process.
  echo "  [WAREHOUSE] Starting pick for order: ", order.Order.id
  for (sku, qty, _) in order.Order.items:
    echo "  [WAREHOUSE]   Pick ", qty, "x ", sku
  result = Picking(order.Order)

proc finishPacking(order: Picking): Packed {.transition.} =
  ## Finish packing the order.
  echo "  [WAREHOUSE] Order packed and ready for shipping"
  result = Packed(order.Order)

proc ship(order: Packed, trackingNumber: string): Shipped {.transition.} =
  ## Hand order to carrier.
  var o = order.Order
  o.trackingNumber = trackingNumber
  echo "  [SHIPPING] Order shipped!"
  echo "  [SHIPPING] Tracking: ", trackingNumber
  result = Shipped(o)

proc confirmDelivery(order: Shipped): Delivered {.transition.} =
  ## Confirm customer received the order.
  echo "  [SHIPPING] Delivery confirmed for: ", order.Order.id
  result = Delivered(order.Order)

# ============================================================================
# Returns
# ============================================================================

proc initiateReturn(order: Delivered, reason: string): Returned {.transition.} =
  ## Customer initiates a return.
  echo "  [RETURNS] Return initiated for: ", order.Order.id
  echo "  [RETURNS] Reason: ", reason
  echo "  [RETURNS] Send to: 123 Returns Center, Warehouse City"
  result = Returned(order.Order)

# ============================================================================
# Status Queries
# ============================================================================

func orderId(order: OrderStates): string =
  ## Get order ID.
  order.Order.id

func trackingNumber(order: Shipped): string =
  ## Get tracking number.
  order.Order.trackingNumber

func total(order: OrderStates): int =
  ## Get order total.
  order.Order.total

# ============================================================================
# Example Usage
# ============================================================================

when isMainModule:
  echo "=== E-commerce Order Fulfillment Demo ===\n"

  echo "1. Customer adds items to cart..."
  let cart = newOrder("cust_12345")
    .addItem("SKU-LAPTOP", 1, 99900)
    .addItem("SKU-MOUSE", 2, 2500)
    .addItem("SKU-CABLE", 3, 1000)

  echo "\n2. Customer places order..."
  let placed = cart.placeOrder()

  echo "\n3. Payment processing..."
  let paid = placed.pay("pay_ch_abc123")

  echo "\n4. Warehouse picks items..."
  let picking = paid.startPicking()

  echo "\n5. Items packed..."
  let packed = picking.finishPacking()

  echo "\n6. Shipped to customer..."
  let shipped = packed.ship("1Z999AA10123456784")

  echo "\n7. Customer receives package..."
  let delivered = shipped.confirmDelivery()

  echo "\n=== Order complete! ===\n"

  # =========================================================================
  # COMPILE-TIME ERRORS - These business logic bugs are prevented:
  # =========================================================================

  echo "The following bugs are caught at COMPILE TIME:\n"

  # BUG 1: Ship before payment
  # let bad1 = placed.ship("TRACKING")
  echo "  [PREVENTED] ship() before payment - lost merchandise!"

  # BUG 2: Refund an order that wasn't paid
  # let bad2 = placed.cancelWithRefund("changed mind")
  echo "  [PREVENTED] cancelWithRefund() on unpaid order"

  # BUG 3: Ship already-shipped order (double ship)
  # let bad3 = shipped.ship("ANOTHER-TRACKING")
  echo "  [PREVENTED] ship() twice - inventory nightmare!"

  # BUG 4: Return before delivery
  # let bad4 = shipped.initiateReturn("don't want it")
  echo "  [PREVENTED] initiateReturn() before delivery"

  # BUG 5: Continue fulfillment after cancellation
  # let cancelled = paid.cancelWithRefund("out of stock")
  # let bad5 = cancelled.startPicking()
  echo "  [PREVENTED] startPicking() after cancellation"

  # BUG 6: Pay for cart (not yet an order)
  # let bad6 = cart.pay("payment")
  echo "  [PREVENTED] pay() on Cart - order not submitted"

  echo "\nUncomment any of the 'bad' lines to see the compile error!"

View full source


Document Workflow

Document publishing enforces a review process: draft, review, approve, publish. Typestates prevent publishing without approval or editing published content.

Bugs prevented: Publishing without approval, editing published content, skipping review process.

## Document Workflow with Typestates
##
## Content publishing workflows have strict rules:
## - Publishing drafts without review: quality issues
## - Editing published content: audit/compliance problems
## - Approving your own work: process violation
## - Skipping required reviews: legal risk
##
## This example models a multi-stage document review workflow.

import ../src/typestates
import std/hashes

type
  Document = object
    id: string
    title: string
    content: string
    author: string
    reviewers: seq[string]
    approver: string
    version: int
    publishedAt: int64

  # Document states
  Draft = distinct Document           ## Being written/edited
  InReview = distinct Document        ## Submitted for review
  ChangesRequested = distinct Document ## Reviewer requested changes
  Approved = distinct Document        ## Passed review, ready to publish
  Published = distinct Document       ## Live/public
  Archived = distinct Document        ## Removed from public, preserved

typestate Document:
  # Documents need to be inspected at various stages without consuming them.
  consumeOnTransition = false
  states Draft, InReview, ChangesRequested, Approved, Published, Archived
  transitions:
    Draft -> InReview                    # Submit for review
    InReview -> (Approved | ChangesRequested) as ReviewResult  # Review decision
    ChangesRequested -> InReview         # Resubmit after changes
    Approved -> Published                # Go live
    Published -> (Archived | Draft) as PublishAction        # Archive or create new version
    Archived -> Draft                    # Restore for new version

# ============================================================================
# Creating and Editing
# ============================================================================

proc newDocument(title: string, author: string): Draft =
  ## Create a new document draft.
  echo "  [DOC] Created: '", title, "' by ", author
  result = Draft(Document(
    id: "doc_" & $hash(title),
    title: title,
    author: author,
    version: 1
  ))

proc edit(doc: Draft, content: string): Draft {.notATransition.} =
  ## Edit the document content.
  var d = doc.Document
  d.content = content
  echo "  [DOC] Updated content (", content.len, " chars)"
  result = Draft(d)

proc setTitle(doc: Draft, title: string): Draft {.notATransition.} =
  ## Update the document title.
  var d = doc.Document
  d.title = title
  echo "  [DOC] Title changed to: '", title, "'"
  result = Draft(d)

# ============================================================================
# Review Process
# ============================================================================

proc submitForReview(doc: Draft, reviewers: seq[string]): InReview {.transition.} =
  ## Submit document for review.
  var d = doc.Document
  d.reviewers = reviewers
  echo "  [DOC] Submitted for review"
  echo "  [DOC] Reviewers: ", reviewers
  result = InReview(d)

proc approve(doc: InReview, approver: string): Approved {.transition.} =
  ## Approve the document.
  var d = doc.Document
  d.approver = approver
  echo "  [DOC] Approved by: ", approver
  result = Approved(d)

proc requestChanges(doc: InReview, feedback: string): ChangesRequested {.transition.} =
  ## Request changes to the document.
  echo "  [DOC] Changes requested: ", feedback
  result = ChangesRequested(doc.Document)

proc updateContent(doc: ChangesRequested, newContent: string): ChangesRequested {.notATransition.} =
  ## Update content while in ChangesRequested state.
  var d = doc.Document
  d.content = newContent
  echo "  [DOC] Content updated (", newContent.len, " chars)"
  result = ChangesRequested(d)

proc resubmit(doc: ChangesRequested): InReview {.transition.} =
  ## Resubmit after making changes.
  echo "  [DOC] Resubmitted for review"
  result = InReview(doc.Document)

# ============================================================================
# Publishing
# ============================================================================

proc publish(doc: Approved): Published {.transition.} =
  ## Publish the approved document.
  var d = doc.Document
  d.publishedAt = 1234567890  # In real code: current timestamp
  echo "  [DOC] Published! '", d.title, "'"
  result = Published(d)

proc archive(doc: Published, reason: string): Archived {.transition.} =
  ## Archive a published document.
  echo "  [DOC] Archived: ", reason
  result = Archived(doc.Document)

proc createNewVersion(doc: Published): Draft {.transition.} =
  ## Create a new draft version from published.
  var d = doc.Document
  d.version += 1
  d.approver = ""
  d.publishedAt = 0
  echo "  [DOC] New version ", d.version, " created from published"
  result = Draft(d)

proc restore(doc: Archived): Draft {.transition.} =
  ## Restore archived document as new draft.
  var d = doc.Document
  d.version += 1
  echo "  [DOC] Restored as version ", d.version
  result = Draft(d)

# ============================================================================
# Status Queries
# ============================================================================

func title(doc: DocumentStates): string =
  doc.Document.title

func author(doc: DocumentStates): string =
  doc.Document.author

func version(doc: DocumentStates): int =
  doc.Document.version

func isPublished(doc: Published): bool = true

# ============================================================================
# Example Usage
# ============================================================================

when isMainModule:
  echo "=== Document Workflow Demo ===\n"

  echo "1. Creating new document..."
  let draft = newDocument("Q4 Strategy Document", "alice@company.com")
    .edit("# Q4 Strategy\n\nOur goals for Q4 are...")
    .setTitle("Q4 2024 Strategy Document")

  echo "\n2. Submitting for review..."
  let review = draft.submitForReview(@["bob@company.com", "carol@company.com"])

  echo "\n3. Reviewer requests changes..."
  let needsChanges = review.requestChanges("Please add budget section")

  echo "\n4. Author makes changes and resubmits..."
  let resubmitted = needsChanges.resubmit()

  echo "\n5. Document approved..."
  let approved = resubmitted.approve("carol@company.com")

  echo "\n6. Publishing document..."
  let published = approved.publish()

  echo "\n7. Later, creating new version for updates..."
  let v2Draft = published.createNewVersion()
  let v2 = v2Draft.edit("# Q4 2024 Strategy\n\nUpdated with Q3 results...")

  echo "\n=== Workflow complete! ===\n"

  # =========================================================================
  # COMPILE-TIME ERRORS - These process violations are prevented:
  # =========================================================================

  echo "The following process violations are caught at COMPILE TIME:\n"

  # BUG 1: Publishing without approval
  # let bad1 = draft.publish()
  echo "  [PREVENTED] publish() Draft - must be approved first!"

  # BUG 2: Publishing during review
  # let bad2 = review.publish()
  echo "  [PREVENTED] publish() InReview - review not complete!"

  # BUG 3: Editing published content
  # let bad3 = published.edit("hacked content")
  echo "  [PREVENTED] edit() Published - audit violation!"

  # BUG 4: Approving draft (skipping review)
  # let bad4 = draft.approve("alice")
  echo "  [PREVENTED] approve() Draft - must go through review!"

  # BUG 5: Double-publishing
  # let bad5 = published.publish()
  echo "  [PREVENTED] publish() Published - already live!"

  # BUG 6: Requesting changes on approved doc
  # let bad6 = approved.requestChanges("wait, one more thing")
  echo "  [PREVENTED] requestChanges() Approved - too late!"

  echo "\nUncomment any of the 'bad' lines to see the compile error!"

View full source


Single-Use Token (Ownership)

Some resources should only be used once: password reset tokens, one-time payment links, event tickets. This example uses consumeOnTransition = true (the default) to enforce that tokens cannot be copied or reused.

Bugs prevented: Double consumption, copying to bypass single-use, using after consumption.

## Single-Use Token with Ownership Enforcement
##
## Some resources should only be used once:
## - Password reset tokens
## - One-time payment links
## - Single-use API keys
## - Event tickets
##
## This example uses consumeOnTransition = true (the default) to enforce
## that tokens cannot be copied or reused after consumption.
##
## Run: nim c -r examples/single_use_token.nim

import ../src/typestates

type
  Token = object
    id: string
    value: string
    createdAt: string

  # Token states
  Valid = distinct Token      ## Token is valid, can be used
  Used = distinct Token       ## Token has been consumed
  Expired = distinct Token    ## Token has expired
  Revoked = distinct Token    ## Token was manually revoked

typestate Token:
  # DEFAULT: consumeOnTransition = true
  # This enforces ownership - tokens cannot be copied after creation.
  # Each token can only follow ONE path through the state machine.
  states Valid, Used, Expired, Revoked
  initial: Valid
  terminal: Used
  transitions:
    Valid -> Used       # Consume the token
    Valid -> Expired    # Token expires
    Valid -> Revoked    # Token is revoked

# ============================================================================
# Token Operations
# ============================================================================

proc createToken(id: string, value: string): Valid =
  ## Create a new single-use token.
  echo "  [TOKEN] Created: ", id
  Valid(Token(id: id, value: value, createdAt: "now"))

proc consume(token: sink Valid): Used {.transition.} =
  ## Use the token (one-time only).
  ## sink + consumeOnTransition = true prevents copying, enforcing single-use.
  let t = Token(token)
  echo "  [TOKEN] Consumed: ", t.id
  Used(t)

proc expire(token: Valid): Expired {.transition.} =
  ## Mark token as expired.
  echo "  [TOKEN] Expired: ", Token(token).id
  Expired(Token(token))

proc revoke(token: Valid): Revoked {.transition.} =
  ## Revoke the token.
  echo "  [TOKEN] Revoked: ", Token(token).id
  Revoked(Token(token))

proc getValue(token: Valid): string {.notATransition.} =
  ## Read the token value (only when valid).
  Token(token).value

# ============================================================================
# Example: Password Reset Token
# ============================================================================

when isMainModule:
  echo "=== Single-Use Token Demo ===\n"

  echo "1. Creating and immediately consuming a password reset token..."
  # With consumeOnTransition = true, tokens flow directly through transitions
  let usedToken = createToken("reset-abc123", "secret-reset-value").consume()

  echo "\n=== Token consumed! ===\n"

  # =========================================================================
  # COMPILE-TIME ERRORS - Ownership enforcement prevents these bugs:
  # =========================================================================

  echo "The following bugs are caught at COMPILE TIME:\n"

  # BUG 1: Storing a token and then trying to use it twice
  # let token = createToken("test", "value")
  # let used1 = token.consume()
  # let used2 = token.consume()  # ERROR: token was already moved
  echo "  [PREVENTED] Double consumption of token"

  # BUG 2: Copying token to bypass single-use
  # let token = createToken("test", "value")
  # let backup = token  # ERROR: =copy is not available for Valid
  echo "  [PREVENTED] Copying token to bypass single-use"

  # BUG 3: Reading token value then consuming (uses token twice)
  # let token = createToken("test", "value")
  # echo token.getValue()
  # discard token.consume()  # ERROR: token was not last read in getValue()
  echo "  [PREVENTED] Reading token value prevents later consumption"

  # BUG 4: Using terminal state
  # let reused = usedToken.consume()  # ERROR: Used is a terminal state
  echo "  [PREVENTED] Transitioning from terminal state"

  echo "\nUncomment any of the 'bad' lines above to see the compile error!"
  echo "\n=== Ownership enforcement ensures tokens are truly single-use ==="

View full source


Shared Session (ref types)

When multiple parts of code need access to the same stateful object, use ref types. Common for session objects, connection pools, and shared resources in async code.

Bugs prevented: Operations on wrong session state, accessing expired sessions.

## Shared Session with ref Types
##
## When multiple parts of your code need access to the same stateful object,
## use `ref` types. This is common for:
## - Session objects shared across handlers
## - Connection pools
## - Shared resources in async code
##
## This example shows how typestates work with heap-allocated ref types.
##
## Run: nim c -r examples/shared_session.nim

import ../src/typestates

type
  Session = object
    id: string
    userId: int
    data: string

  # Session states
  Unauthenticated = distinct Session
  Authenticated = distinct Session
  Expired = distinct Session

typestate Session:
  # Shared sessions need to be read from multiple places
  consumeOnTransition = false
  states Unauthenticated, Authenticated, Expired
  transitions:
    Unauthenticated -> Authenticated
    Authenticated -> Expired

# ============================================================================
# Session Operations (ref types)
# ============================================================================

proc newSession(id: string): ref Unauthenticated =
  ## Create a new heap-allocated session.
  result = new(Unauthenticated)
  result[] = Unauthenticated(Session(id: id))
  echo "  [SESSION] Created: ", id

proc authenticate(session: ref Unauthenticated, userId: int): ref Authenticated {.transition.} =
  ## Authenticate the session - works with ref types.
  var s = Session(session[])
  s.userId = userId
  result = new(Authenticated)
  result[] = Authenticated(s)
  echo "  [SESSION] Authenticated user ", userId

proc expire(session: ref Authenticated): ref Expired {.transition.} =
  ## Expire the session.
  result = new(Expired)
  result[] = Expired(Session(session[]))
  echo "  [SESSION] Expired"

proc setData(session: ref Authenticated, data: string) =
  ## Modify session data (only when authenticated).
  var s = Session(session[])
  s.data = data
  session[] = Authenticated(s)
  echo "  [SESSION] Data set: ", data

proc getData(session: ref Authenticated): string =
  ## Read session data.
  Session(session[]).data

proc getUserId(session: ref Authenticated): int =
  ## Get the authenticated user ID.
  Session(session[]).userId

# ============================================================================
# Example: Multiple references to same session
# ============================================================================

when isMainModule:
  echo "=== Shared Session Demo (ref types) ===\n"

  echo "1. Creating session..."
  let session = newSession("sess-abc123")

  echo "\n2. Authenticating..."
  let authSession = session.authenticate(42)

  echo "\n3. Multiple parts of code can access the same session..."
  # Simulate different parts of the application using the session
  authSession.setData("user preferences")
  echo "   Handler A reads: ", authSession.getData()
  echo "   Handler B reads user: ", authSession.getUserId()

  echo "\n4. Expiring session..."
  let expiredSession = authSession.expire()

  echo "\n=== Session lifecycle complete! ===\n"

  # =========================================================================
  # COMPILE-TIME ERRORS
  # =========================================================================

  echo "The following bugs are caught at COMPILE TIME:\n"

  # BUG 1: Setting data on unauthenticated session
  # session.setData("hack")  # ERROR: no matching proc for ref Unauthenticated
  echo "  [PREVENTED] setData() on unauthenticated session"

  # BUG 2: Getting user ID from expired session
  # echo expiredSession.getUserId()  # ERROR: no matching proc for ref Expired
  echo "  [PREVENTED] getUserId() on expired session"

  # BUG 3: Authenticating already-authenticated session
  # discard authSession.authenticate(99)  # ERROR: no matching proc
  echo "  [PREVENTED] authenticate() on already-authenticated session"

  echo "\nRef types work seamlessly with typestates!"

View full source


Hardware Register (ptr types)

When interfacing with hardware or memory-mapped I/O, typestates can enforce correct access patterns with raw pointers.

Bugs prevented: Reading uninitialized registers, modifying locked registers.

## Hardware Register Access with ptr Types
##
## When interfacing with hardware or memory-mapped I/O, you often work with
## raw pointers. Typestates can enforce correct access patterns:
## - Registers must be initialized before use
## - Some registers are read-only after configuration
## - Certain sequences must be followed
##
## This example shows how typestates work with ptr types for low-level code.
##
## Run: nim c -r examples/hardware_register.nim

import std/strutils
import ../src/typestates

type
  Register = object
    address: uint32
    value: uint32

  # Register states
  Uninitialized = distinct Register
  Configured = distinct Register
  Locked = distinct Register

typestate Register:
  # Hardware registers are accessed via pointers
  consumeOnTransition = false
  states Uninitialized, Configured, Locked
  transitions:
    Uninitialized -> Configured
    Configured -> Configured   # Can reconfigure
    Configured -> Locked       # Lock to prevent further changes

# ============================================================================
# Register Operations (ptr types)
# ============================================================================

proc initRegister(reg: ptr Uninitialized, value: uint32): ptr Configured {.transition.} =
  ## Initialize a hardware register with a value.
  echo "  [REG 0x", reg[].Register.address.toHex, "] Init: 0x", value.toHex
  var r = Register(reg[])
  r.value = value
  reg[] = Uninitialized(r)
  cast[ptr Configured](../../examples/reg)

proc configure(reg: ptr Configured, value: uint32): ptr Configured {.transition.} =
  ## Reconfigure a register (only when not locked).
  echo "  [REG 0x", reg[].Register.address.toHex, "] Configure: 0x", value.toHex
  var r = Register(reg[])
  r.value = value
  reg[] = Configured(r)
  reg

proc lock(reg: ptr Configured): ptr Locked {.transition.} =
  ## Lock the register to prevent further modifications.
  echo "  [REG 0x", reg[].Register.address.toHex, "] LOCKED"
  cast[ptr Locked](../../examples/reg)

proc read(reg: ptr Configured): uint32 =
  ## Read value from configured register.
  Register(reg[]).value

proc read(reg: ptr Locked): uint32 =
  ## Read value from locked register.
  Register(reg[]).value

# ============================================================================
# Example: GPIO Configuration
# ============================================================================

when isMainModule:
  echo "=== Hardware Register Demo (ptr types) ===\n"

  # Simulate memory-mapped registers
  var gpioModeReg = Uninitialized(Register(address: 0x4002_0000'u32, value: 0))
  var gpioSpeedReg = Uninitialized(Register(address: 0x4002_0008'u32, value: 0))

  echo "1. Initializing GPIO registers..."
  let modePtr = addr(gpioModeReg).initRegister(0x0000_0001)  # Output mode
  let speedPtr = addr(gpioSpeedReg).initRegister(0x0000_0003)  # High speed

  echo "\n2. Reading configured values..."
  echo "   Mode register: 0x", modePtr.read().toHex
  echo "   Speed register: 0x", speedPtr.read().toHex

  echo "\n3. Reconfiguring mode register..."
  let modePtr2 = modePtr.configure(0x0000_0002)  # Alternate function mode
  echo "   New mode: 0x", modePtr2.read().toHex

  echo "\n4. Locking speed register..."
  let lockedSpeed = speedPtr.lock()
  echo "   Locked value: 0x", lockedSpeed.read().toHex

  echo "\n=== Register configuration complete! ===\n"

  # =========================================================================
  # COMPILE-TIME ERRORS
  # =========================================================================

  echo "The following bugs are caught at COMPILE TIME:\n"

  # BUG 1: Reading uninitialized register
  # var uninit = Uninitialized(Register(address: 0xDEAD, value: 0))
  # echo addr(uninit).read()  # ERROR: no matching proc for ptr Uninitialized
  echo "  [PREVENTED] read() on uninitialized register"

  # BUG 2: Configuring locked register
  # discard lockedSpeed.configure(0xFF)  # ERROR: no matching proc for ptr Locked
  echo "  [PREVENTED] configure() on locked register"

  # BUG 3: Locking uninitialized register
  # var uninit2 = Uninitialized(Register(address: 0xBEEF, value: 0))
  # discard addr(uninit2).lock()  # ERROR: no matching proc for ptr Uninitialized
  echo "  [PREVENTED] lock() on uninitialized register"

  echo "\nPtr types enable type-safe hardware access!"

View full source


Generic Patterns

Reusable typestate patterns using generics. See Generic Typestates for more details.

Resource[T] Pattern

A reusable pattern for any resource requiring acquire/release semantics.

## Generic Resource[T] Pattern
##
## A reusable typestate pattern for any resource that must be acquired
## before use and released after. Works with file handles, locks,
## connections, memory allocations, or any RAII-style resource.
##
## Run: nim c -r examples/generic_resource.nim

import ../src/typestates

# =============================================================================
# Generic Resource Pattern
# =============================================================================

type
  Resource*[T] = object
    ## Base type holding any resource.
    handle*: T
    name*: string  # For diagnostics

  Released*[T] = distinct Resource[T]
    ## Resource is not held - cannot be used.

  Acquired*[T] = distinct Resource[T]
    ## Resource is held - can be used, must be released.

typestate Resource[T]:
  # Resources can be acquired, released, and re-acquired (RAII pattern).
  consumeOnTransition = false
  states Released[T], Acquired[T]
  transitions:
    Released[T] -> Acquired[T]
    Acquired[T] -> Released[T]

proc acquire*[T](../../examples/r: Released[T], handle: T): Acquired[T] {.transition.} =
  ## Acquire the resource with the given handle.
  var res = Resource[T](../../examples/r)
  res.handle = handle
  echo "[", res.name, "] Acquired"
  result = Acquired[T](../../examples/res)

proc release*[T](../../examples/r: Acquired[T]): Released[T] {.transition.} =
  ## Release the resource back.
  echo "[", Resource[T](../../examples/r).name, "] Released"
  result = Released[T](../../examples/Resource[T](r))

proc use*[T](../../examples/r: Acquired[T]): T {.notATransition.} =
  ## Access the underlying handle (only when acquired).
  Resource[T](../../examples/r).handle

proc withResource*[T, R](r: Released[T], handle: T,
                          body: proc(h: T): R): (R, Released[T]) =
  ## RAII-style helper: acquire, use, release automatically.
  let acquired = r.acquire(handle)
  let res = body(acquired.use())
  let released = acquired.release()
  result = (res, released)

# =============================================================================
# Example 1: File Handle
# =============================================================================

type FileHandle = object
  fd: int
  path: string

proc openFile(path: string): FileHandle =
  echo "  Opening: ", path
  FileHandle(fd: 42, path: path)

proc closeFile(fh: FileHandle) =
  echo "  Closing: ", fh.path

proc readFile(fh: FileHandle): string =
  echo "  Reading from fd=", fh.fd
  "file contents"

block fileExample:
  echo "\n=== File Handle Example ==="

  # Create a released resource
  var file = Released[FileHandle](../../examples/Resource[FileHandle](name: "config.txt"))

  # Acquire it
  let handle = openFile("/etc/config.txt")
  let acquired = file.acquire(handle)

  # Use it
  let contents = acquired.use().readFile()
  echo "  Got: ", contents

  # Release it
  let released = acquired.release()
  closeFile(handle)

  # COMPILE ERROR if uncommented:
  # discard released.use()  # Can't use released resource!

# =============================================================================
# Example 2: Database Connection
# =============================================================================

type DbConn = object
  connString: string
  connected: bool

proc connect(connString: string): DbConn =
  echo "  Connecting to: ", connString
  DbConn(connString: connString, connected: true)

proc disconnect(conn: DbConn) =
  echo "  Disconnecting"

proc query(conn: DbConn, sql: string): seq[string] =
  echo "  Query: ", sql
  @["row1", "row2", "row3"]

block dbExample:
  echo "\n=== Database Connection Example ==="

  var db = Released[DbConn](../../examples/Resource[DbConn](name: "postgres"))

  # Manual acquire/release
  let conn = connect("postgresql://localhost/mydb")
  let acquired = db.acquire(conn)

  let rows = acquired.use().query("SELECT * FROM users")
  echo "  Results: ", rows

  let released = acquired.release()
  disconnect(conn)

# =============================================================================
# Example 3: Lock/Mutex simulation
# =============================================================================

type SimpleLock = object
  id: int

proc lock(id: int): SimpleLock =
  echo "  Locking mutex #", id
  SimpleLock(id: id)

proc unlock(l: SimpleLock) =
  echo "  Unlocking mutex #", l.id

block lockExample:
  echo "\n=== Lock Example ==="

  var mutex = Released[SimpleLock](../../examples/Resource[SimpleLock](name: "mutex"))

  # Using withResource for RAII-style usage
  let (result, mutexReleased) = mutex.withResource(lock(1)) do (l: SimpleLock) -> int:
    echo "  Critical section with lock #", l.id
    42  # Return value from critical section

  echo "  Result from critical section: ", result
  unlock(SimpleLock(id: 1))

# =============================================================================
# Example 4: Memory Pool Allocation
# =============================================================================

type PooledBuffer = object
  size: int
  data: ptr UncheckedArray[byte]

proc allocFromPool(size: int): PooledBuffer =
  echo "  Allocating ", size, " bytes from pool"
  # In real code, this would allocate from a pool
  PooledBuffer(size: size, data: nil)

proc returnToPool(buf: PooledBuffer) =
  echo "  Returning ", buf.size, " bytes to pool"

block memoryExample:
  echo "\n=== Memory Pool Example ==="

  var buffer = Released[PooledBuffer](../../examples/Resource[PooledBuffer](name: "buffer"))

  let mem = allocFromPool(4096)
  let acquired = buffer.acquire(mem)

  echo "  Using buffer of size: ", acquired.use().size

  let released = acquired.release()
  returnToPool(mem)

# =============================================================================
# Summary
# =============================================================================

echo "\n=== Summary ==="
echo "The Resource[T] pattern ensures:"
echo "  - Resources must be acquired before use"
echo "  - Resources must be released after use"
echo "  - Compile-time prevention of use-after-release"
echo "  - Works with ANY resource type via generics"

echo "\nAll examples passed!"

View full source


Pipeline[T] Pattern

A reusable pattern for entities that progress through a fixed sequence of stages.

## Generic Pipeline[T] Pattern
##
## A reusable typestate pattern for entities that progress through
## a fixed sequence of stages. Works for orders, documents, builds,
## deployments, or any linear workflow.
##
## Run: nim c -r examples/generic_pipeline.nim

import ../src/typestates

# =============================================================================
# Generic Pipeline Pattern (4-stage)
# =============================================================================

type
  Pipeline*[T] = object
    ## Base type holding the entity progressing through stages.
    entity*: T
    startedAt*: string

  Stage1*[T] = distinct Pipeline[T]
    ## Initial stage - entity just entered the pipeline.

  Stage2*[T] = distinct Pipeline[T]
    ## Second stage - first transition complete.

  Stage3*[T] = distinct Pipeline[T]
    ## Third stage - nearing completion.

  Stage4*[T] = distinct Pipeline[T]
    ## Final stage - pipeline complete.

typestate Pipeline[T]:
  # Pipeline entities flow through stages and may be inspected at each stage.
  consumeOnTransition = false
  states Stage1[T], Stage2[T], Stage3[T], Stage4[T]
  transitions:
    Stage1[T] -> Stage2[T]
    Stage2[T] -> Stage3[T]
    Stage3[T] -> Stage4[T]

proc start*[T](entity: T, timestamp: string): Stage1[T] =
  ## Enter the pipeline at stage 1.
  Stage1[T](../../examples/Pipeline[T](entity: entity, startedAt: timestamp))

proc advance12*[T](../../examples/p: Stage1[T]): Stage2[T] {.transition.} =
  ## Advance from stage 1 to stage 2.
  Stage2[T](../../examples/Pipeline[T](p))

proc advance23*[T](../../examples/p: Stage2[T]): Stage3[T] {.transition.} =
  ## Advance from stage 2 to stage 3.
  Stage3[T](../../examples/Pipeline[T](p))

proc advance34*[T](../../examples/p: Stage3[T]): Stage4[T] {.transition.} =
  ## Advance from stage 3 to stage 4 (complete).
  Stage4[T](../../examples/Pipeline[T](p))

proc entity*[T](../../examples/p: Stage1[T]): T {.notATransition.} = Pipeline[T](../../examples/p).entity
proc entity*[T](../../examples/p: Stage2[T]): T {.notATransition.} = Pipeline[T](../../examples/p).entity
proc entity*[T](../../examples/p: Stage3[T]): T {.notATransition.} = Pipeline[T](../../examples/p).entity
proc entity*[T](../../examples/p: Stage4[T]): T {.notATransition.} = Pipeline[T](../../examples/p).entity

# =============================================================================
# Example 1: Order Fulfillment
# =============================================================================

type
  Order = object
    id: string
    items: seq[string]
    total: int

  # Semantic aliases for order stages
  OrderCart = Stage1[Order]
  OrderPaid = Stage2[Order]
  OrderShipped = Stage3[Order]
  OrderDelivered = Stage4[Order]

proc addItem(cart: OrderCart, item: string, price: int): OrderCart {.notATransition.} =
  var order = cart.entity()
  order.items.add(item)
  order.total += price
  Stage1[Order](../../examples/Pipeline[Order](entity: order, startedAt: Pipeline[Order](cart).startedAt))

proc pay(cart: OrderCart): OrderPaid =
  echo "  Payment received: $", cart.entity().total
  cart.advance12()

proc ship(order: OrderPaid, tracking: string): OrderShipped =
  echo "  Shipped with tracking: ", tracking
  order.advance23()

proc deliver(order: OrderShipped): OrderDelivered =
  echo "  Delivered!"
  order.advance34()

block orderExample:
  echo "\n=== Order Fulfillment Example ==="

  let cart = start(Order(id: "ORD-001"), "2024-01-15")
    .addItem("Laptop", 999)
    .addItem("Mouse", 29)
    .addItem("Keyboard", 79)

  echo "  Cart total: $", cart.entity().total

  let paid = cart.pay()
  let shipped = paid.ship("1Z999AA10123456784")
  let delivered = shipped.deliver()

  echo "  Order ", delivered.entity().id, " complete!"

  # COMPILE ERRORS if uncommented:
  # discard cart.ship("TRACK")      # Can't ship unpaid cart
  # discard shipped.pay()           # Can't pay already-shipped order
  # discard delivered.advance34()   # Can't advance past final stage

# =============================================================================
# Example 2: CI/CD Build Pipeline
# =============================================================================

type
  Build = object
    repo: string
    commit: string
    artifacts: seq[string]

  # Semantic aliases for build stages
  BuildQueued = Stage1[Build]
  BuildCompiling = Stage2[Build]
  BuildTesting = Stage3[Build]
  BuildDeployed = Stage4[Build]

proc startBuild(repo, commit: string): BuildQueued =
  echo "  Build queued for ", repo, "@", commit[0..6]
  start(Build(repo: repo, commit: commit), "now")

proc compile(build: BuildQueued): BuildCompiling =
  echo "  Compiling..."
  var b = build.entity()
  b.artifacts.add("app.bin")
  let pipeline = Pipeline[Build](entity: b, startedAt: Pipeline[Build](build).startedAt)
  Stage2[Build](../../examples/pipeline)

proc test(build: BuildCompiling): BuildTesting =
  echo "  Running tests..."
  build.advance23()

proc deploy(build: BuildTesting): BuildDeployed =
  echo "  Deploying artifacts: ", build.entity().artifacts
  build.advance34()

block buildExample:
  echo "\n=== CI/CD Build Pipeline Example ==="

  let deployed = startBuild("github.com/user/project", "abc123def456")
    .compile()
    .test()
    .deploy()

  echo "  Build complete for ", deployed.entity().repo

  # COMPILE ERRORS:
  # discard startBuild("repo", "commit").deploy()  # Can't skip stages!

# =============================================================================
# Example 3: Document Review
# =============================================================================

type
  Document = object
    title: string
    content: string
    reviewer: string
    approver: string

  # Semantic aliases
  DocDraft = Stage1[Document]
  DocInReview = Stage2[Document]
  DocApproved = Stage3[Document]
  DocPublished = Stage4[Document]

proc createDraft(title: string): DocDraft =
  start(Document(title: title), "draft-created")

proc edit(doc: DocDraft, content: string): DocDraft {.notATransition.} =
  var d = doc.entity()
  d.content = content
  Stage1[Document](../../examples/Pipeline[Document](entity: d, startedAt: Pipeline[Document](doc).startedAt))

proc submitForReview(doc: DocDraft, reviewer: string): DocInReview =
  var d = doc.entity()
  d.reviewer = reviewer
  echo "  Submitted to ", reviewer, " for review"
  let pipeline = Pipeline[Document](entity: d, startedAt: Pipeline[Document](doc).startedAt)
  Stage2[Document](../../examples/pipeline)

proc approve(doc: DocInReview, approver: string): DocApproved =
  var d = doc.entity()
  d.approver = approver
  echo "  Approved by ", approver
  let pipeline = Pipeline[Document](entity: d, startedAt: Pipeline[Document](doc).startedAt)
  Stage3[Document](../../examples/pipeline)

proc publish(doc: DocApproved): DocPublished =
  echo "  Published: ", doc.entity().title
  doc.advance34()

block docExample:
  echo "\n=== Document Review Example ==="

  let published = createDraft("Q4 Strategy")
    .edit("Our goals for Q4 include...")
    .submitForReview("alice@company.com")
    .approve("bob@company.com")
    .publish()

  echo "  Document '", published.entity().title, "' is live!"

  # COMPILE ERRORS:
  # discard createDraft("Doc").publish()  # Can't publish without review!

# =============================================================================
# Example 4: Deployment Pipeline
# =============================================================================

type
  Deployment = object
    service: string
    version: string
    environment: string

  # Semantic aliases
  DeployStaging = Stage1[Deployment]
  DeployCanary = Stage2[Deployment]
  DeployPartial = Stage3[Deployment]
  DeployFull = Stage4[Deployment]

proc deployToStaging(service, version: string): DeployStaging =
  echo "  Deploying ", service, " v", version, " to staging"
  start(Deployment(service: service, version: version, environment: "staging"), "now")

proc promoteToCanary(d: DeployStaging): DeployCanary =
  echo "  Promoting to canary (1% traffic)"
  var dep = d.entity()
  dep.environment = "canary"
  Stage2[Deployment](../../examples/Pipeline[Deployment](entity: dep, startedAt: "now"))

proc expandToPartial(d: DeployCanary): DeployPartial =
  echo "  Expanding to 25% traffic"
  var dep = d.entity()
  dep.environment = "partial"
  Stage3[Deployment](../../examples/Pipeline[Deployment](entity: dep, startedAt: "now"))

proc rolloutFull(d: DeployPartial): DeployFull =
  echo "  Full rollout to 100% traffic"
  var dep = d.entity()
  dep.environment = "production"
  Stage4[Deployment](../../examples/Pipeline[Deployment](entity: dep, startedAt: "now"))

block deployExample:
  echo "\n=== Deployment Pipeline Example ==="

  let production = deployToStaging("api-server", "2.3.0")
    .promoteToCanary()
    .expandToPartial()
    .rolloutFull()

  echo "  ", production.entity().service, " v", production.entity().version,
       " is now in ", production.entity().environment

# =============================================================================
# Summary
# =============================================================================

echo "\n=== Summary ==="
echo "The Pipeline[T] pattern ensures:"
echo "  - Entities must progress through stages in order"
echo "  - No stage can be skipped"
echo "  - Operations are only valid at appropriate stages"
echo "  - Works with ANY entity type via generics"
echo ""
echo "Common applications:"
echo "  - Order fulfillment (Cart -> Paid -> Shipped -> Delivered)"
echo "  - CI/CD builds (Queue -> Compile -> Test -> Deploy)"
echo "  - Document review (Draft -> Review -> Approve -> Publish)"
echo "  - Canary deployments (Staging -> Canary -> Partial -> Full)"

echo "\nAll examples passed!"

View full source


Tips for Designing Typestates

1. Start with the State Diagram

Draw your states and transitions first. Each arrow becomes a transition declaration.

2. One Responsibility Per State

Each state should represent one clear condition. If a state has multiple meanings, split it.

3. Use Wildcards Sparingly

* -> X is powerful but can hide bugs. Use it only for truly universal operations like "reset" or "emergency stop".

4. Consider Error States

Many real systems need error/failure states. Plan for them upfront.

5. Document State Meanings

Even with types enforcing transitions, document what each state means:

type
  Pending = distinct Order
    ## Order placed but not paid

  Paid = distinct Order
    ## Payment received, awaiting fulfillment

  Shipped = distinct Order
    ## Order shipped to customer