DSL Overview
DSL Overview
Section titled “DSL Overview”scaf uses a declarative domain-specific language designed for readability and expressiveness. This page provides a complete overview of the language structure.
File Structure
Section titled “File Structure”A scaf file consists of these sections, in order:
// 1. Imports (optional)import fixtures "./shared/fixtures"
// 2. Function definitionsfn GetUser(userId: string) `MATCH (u:User {id: $userId})RETURN u.name`
// 3. Global setup (optional)setup `CREATE (:User {id: 1, name: "Alice"})`
// 4. Global teardown (optional)teardown `MATCH (n) DETACH DELETE n`
// 5. Function scopes with testsGetUser { test "finds user" { $userId: 1 u.name: "Alice" }}Syntax Elements
Section titled “Syntax Elements”Comments
Section titled “Comments”Single-line comments start with //:
// This is a commentfn GetUser(userId) `...` // Inline commentIdentifiers
Section titled “Identifiers”Identifiers follow standard rules:
- Start with a letter or underscore
- Contain letters, digits, or underscores
- Case-sensitive
fn GetUserById(id) `...` // Validfn get_user_2(id) `...` // Validfn 2users(id) `...` // Invalid - starts with digitStrings
Section titled “Strings”Two string formats:
Quoted strings — For test names, imports, and values:
import "path/to/module"test "my test name" { ... }u.name: "Alice"u.status: 'active' // Single quotes also workRaw strings — For query bodies (backtick-delimited):
fn GetUser(userId) `MATCH (u:User {id: $userId})RETURN u.name, u.email`Numbers
Section titled “Numbers”Multiple number formats supported:
age: 42 // Integerprice: 3.14 // Floatid: 0xFF // Hexadecimalmask: 0b1010 // Binaryperms: 0o755 // Octalbig: 1_000_000 // Underscores for readabilityexp: 1.5e10 // Scientific notationnegative: -42 // Negative numbersBooleans and Null
Section titled “Booleans and Null”verified: truedeleted: falsenickname: nullCollections
Section titled “Collections”Lists:
tags: ["tag1", "tag2", "tag3"]ids: [1, 2, 3]mixed: [1, "two", true, null]Maps:
metadata: {key: "value", count: 42}nested: {outer: {inner: "value"}}Functions
Section titled “Functions”Define named database queries:
fn FunctionName(params) ` -- Your database query here -- Parameters use $paramName syntax`Example with Cypher:
fn GetUserWithPosts(userId: string) `MATCH (u:User {id: $userId})OPTIONAL MATCH (u)-[:AUTHORED]->(p:Post)RETURN u.name, collect(p.title) as posts`Test Scopes
Section titled “Test Scopes”Group tests by the function they exercise:
GetUserWithPosts { // Tests and groups go here
test "user with posts" { $userId: 1 u.name: "Alice" posts: ["Post 1", "Post 2"] }}The scope name must exactly match a defined function name.
Groups
Section titled “Groups”Organize related tests:
GetUser { group "happy path" { test "finds existing user" { ... } test "returns all fields" { ... } }
group "edge cases" { test "handles missing user" { ... } test "handles invalid id" { ... } }
// Groups can nest group "permissions" { group "admin users" { test "can see all fields" { ... } } group "regular users" { test "sees limited fields" { ... } } }}The core unit. Each test specifies inputs, expected outputs, and optional assertions:
test "descriptive test name" { // Optional: test-specific setup setup `CREATE (:TempNode)`
// Input parameters (prefixed with $) $userId: 1 $includeDeleted: false
// Expected outputs (field paths) u.name: "Alice" u.email: "alice@example.com" u.age: 30 u.verified: true u.tags: ["admin", "user"]
// Optional: expression assertions assert { (u.age >= 18) (u.email contains "@") }
// Optional: query assertions assert `MATCH (p:Post) RETURN count(p) as cnt` { (cnt > 0) }}Setup and Teardown
Section titled “Setup and Teardown”Run code before and after tests:
Inline Setup
Section titled “Inline Setup”setup `CREATE (:User {id: 1, name: "Alice"})CREATE (:User {id: 2, name: "Bob"})`
teardown `MATCH (n) DETACH DELETE n`Named Setup
Section titled “Named Setup”Reference setups defined in other files:
import fixtures "./shared/fixtures"
setup fixtures.CreateUsers()
// With parameterssetup fixtures.CreatePosts(count: 10, authorId: 1)Setup Levels
Section titled “Setup Levels”Setup can be defined at multiple levels:
// Suite level - runs once for entire filesetup `...`
FunctionName { // Scope level - runs for this function scope setup `...`
group "scenario" { // Group level - runs for this group setup `...`
test "case" { // Test level - runs for this test only setup `...` } }}Assertions
Section titled “Assertions”Two types of assertions:
Expression Assertions
Section titled “Expression Assertions”Validate output values using expressions:
assert { (u.age >= 18) (u.email contains "@") (len(u.name) > 0) (u.status == "active" || u.status == "pending")}Query Assertions
Section titled “Query Assertions”Run additional queries and validate their results:
// Inline queryassert `MATCH (p:Post) WHERE p.authorId = 1 RETURN count(p) as cnt` { (cnt > 0)}
// Named functionassert CountUsers() { (count == 2)}
// Named function with parametersassert GetPostsByAuthor(authorId: 1) { (len(posts) > 0) (posts[0].title != "")}Imports
Section titled “Imports”Reuse setup and fixtures across files:
// Simple importimport "./shared/fixtures"
// Import with aliasimport fixtures "./shared/fixtures"
// Use imported setupssetup fixtures.CreateTestData()See Imports for full details.
Complete Example
Section titled “Complete Example”// Importsimport fixtures "./shared/fixtures"
// Functionsfn GetUser(userId: string) `MATCH (u:User {id: $userId})RETURN u.name, u.email, u.age, u.verified`
fn CountUsers() `MATCH (u:User)RETURN count(u) as cnt`
// Global setupsetup fixtures.CreateUsers()
// Global teardownteardown `MATCH (n) DETACH DELETE n`
// TestsGetUser { setup `CREATE (:Session {userId: 1})`
group "existing users" { test "finds Alice with all fields" { $userId: 1
u.name: "Alice" u.email: "alice@example.com" u.age: 30 u.verified: true
assert { (u.age >= 18) } }
test "finds Bob" { $userId: 2
u.name: "Bob" u.age: 25 } }
group "edge cases" { test "missing user returns null" { $userId: 999
u.name: null u.email: null } }}
CountUsers { test "base count" { cnt: 2 }
test "with temp user" { setup `CREATE (:User {id: 999})`
cnt: 3
assert CountUsers() { (cnt > 2) } }}