Full Application: Product Catalog with State Machine
The capstone example: combines cells, state machines, pipelines, memory, tests, and HTML rendering in one file.
Example 100 of the basic Soma programs: a small, commented program that teaches one thing. Every output below was printed by soma 3.2.4 when this page was built.
How it is built
The cell ProductCatalog keeps 3 memory slots: products (Map<String, String>, persistent and consistent), status (Map<String, String>, persistent and consistent) and meta (Map<String, Int>, persistent and consistent). Its state machine product_status starts in draft and allows 4 transitions: draft -> published, published -> archived, archived -> published and * -> deleted. deleted is terminal. Handlers: add_product (line 48), publish (line 63), archive (line 67), get_published (line 71), get_all (line 83) and demo (line 92).
The cell MathHelper has no face and no memory. Handlers: double (line 141) and is_positive (line 145).
The file also holds 1 test cell (CatalogTests) with 4 assertions, run by soma test.
The source
100_full_app.cell
// ============================================================================
// 100 — Full Application: Product Catalog with State Machine
// ============================================================================
// The capstone example: combines cells, state machines, pipelines,
// memory, tests, and HTML rendering in one file.
//
// Run: soma run 100_full_app.cell demo
// Expect:
// === Product Catalog ===
// Added: Laptop ($999)
// Added: Phone ($699)
// Added: Tablet ($449)
// Added: Watch ($299)
// Added: Earbuds ($149)
// Published Laptop
// Published Phone
// Published Tablet
// === Published Products ===
// Laptop: $999
// Phone: $699
// Tablet: $449
// === Analytics ===
// Total value: 2147
// Average price: 715
// Most expensive: Laptop ($999)
//
// Run: soma test 100_full_app.cell
// Expect: All tests pass
cell ProductCatalog {
memory {
products: Map<String, String> [persistent, consistent]
status: Map<String, String> [persistent, consistent]
meta: Map<String, Int> [persistent, consistent]
}
// Product lifecycle state machine
state product_status {
initial: draft
draft -> published
published -> archived
archived -> published // can re-publish
* -> deleted
}
on add_product(name: String, price: Int, category: String) {
let current = meta.get("next_id")
if current == () {
meta.set("next_id", 1)
}
let id = meta.get("next_id")
meta.set("next_id", id + 1)
let id_str = to_string(id)
let product = map("id", id, "name", name, "price", price, "category", category)
products.set(id_str, to_json(product))
status.set(id_str, "draft")
return product
}
on publish(id: String) {
return transition(id, "published")
}
on archive(id: String) {
return transition(id, "archived")
}
on get_published() {
let keys = products.keys()
let result = list()
for key in keys {
let st = status.get(key)
if st == "published" {
result = list(result, from_json(products.get(key)))
}
}
return result |> sort_by("price", "desc")
}
on get_all() {
let keys = products.keys()
let result = list()
for key in keys {
result = list(result, from_json(products.get(key)))
}
return result
}
on demo() {
print("=== Product Catalog ===")
let p1 = add_product("Laptop", 999, "Electronics")
print("Added: {p1.name} (${p1.price})")
let p2 = add_product("Phone", 699, "Electronics")
print("Added: {p2.name} (${p2.price})")
let p3 = add_product("Tablet", 449, "Electronics")
print("Added: {p3.name} (${p3.price})")
let p4 = add_product("Watch", 299, "Accessories")
print("Added: {p4.name} (${p4.price})")
let p5 = add_product("Earbuds", 149, "Accessories")
print("Added: {p5.name} (${p5.price})")
// Publish some products
publish("1")
print("Published Laptop")
publish("2")
print("Published Phone")
publish("3")
print("Published Tablet")
// Show published only
let published = get_published()
print("=== Published Products ===")
for p in published {
print("{p.name}: ${p.price}")
}
// Analytics on published products
let total = published |> sum_by("price")
let avg = published |> avg_by("price")
let most = published |> max_by("price")
print("=== Analytics ===")
print("Total value: {total}")
print("Average price: {avg}")
print("Most expensive: {most.name} (${most.price})")
return "done"
}
}
// Helper for tests
cell MathHelper {
on double(n: Int) {
return n * 2
}
on is_positive(n: Int) {
if n > 0 {
return true
}
return false
}
}
// Tests
cell test CatalogTests {
rules {
assert double(5) == 10
assert double(0) == 0
assert is_positive(1) == true
assert is_positive(0) == false
}
}What the toolchain says
$ soma check 100_full_app.cell
✓ All checks passed.$ soma test 100_full_app.cell
test CatalogTests ...
✓ assert double(5) == 10
✓ assert double(0) == 0
✓ assert is_positive(1) == true
✓ assert is_positive(0) == false
4 tests: 4 passed, 0 failed$ soma run 100_full_app.cell demo
=== Product Catalog ===
Added: Laptop ($999)
Added: Phone ($699)
Added: Tablet ($449)
Added: Watch ($299)
Added: Earbuds ($149)
Published Laptop
Published Phone
Published Tablet
=== Published Products ===
=== Analytics ===
Total value: 0
Average price: null
error: cannot read field 'name' of Unit null — it is not a map or a record
--> 100_full_app.cell:133:9
|
133 | print("Most expensive: {most.name} (${most.price})")
| ^