ForgeDB Parser
Core schema language parser for ForgeDB.
Overview
The forgedb-parser crate is the schema language parser for ForgeDB. It parses .forge schema files into an Abstract Syntax Tree (AST) that can be used for code generation, validation, and analysis. The parser implements a complete lexer and recursive descent parser for the ForgeDB schema language.
Features
- Complete schema parsing - Models, fields, types, and directives
- Comprehensive type system - Primitives, relations, structs, and arrays
- Rich constraints - Validation directives like
@min,@max,@email - Relation support - One-to-many, many-to-many, required and optional references
- Component integration - TSX/JSX component references and API routes
- Composite indexes - Multi-field index definitions
- Detailed error messages - Parse errors with line and column information
- Validation - Model and field name validation
Schema Language
Syntax Overview
ForgeDB schemas define models with fields. Each model represents a database table or collection.
User {
id: +uuid
email: &string
name: string
}
Model Definitions
Models are defined with a name (PascalCase) followed by a block containing fields:
ModelName {
field1: type
field2: type
}
Field Types and Modifiers
Primitive Types
| Type | Description | Example |
|---|---|---|
u32 |
32-bit unsigned integer | count: u32 |
u64 |
64-bit unsigned integer | id: u64 |
i32 |
32-bit signed integer | temperature: i32 |
i64 |
64-bit signed integer | balance: i64 |
f64 |
64-bit floating point | price: f64 |
bool |
Boolean value | active: bool |
string |
UTF-8 text | name: string |
uuid |
UUID identifier | id: uuid |
timestamp |
Unix timestamp | created_at: timestamp |
Fixed-Size Types
Person {
code: char(10) // Fixed-size character array
matrix: [i32; 9] // Fixed-size array
point: Point // Struct reference
optional_data: ?Point // Optional struct
}
Field Modifiers
Field modifiers are symbols that appear before the type:
+- Auto-generate: Automatically generate value on creation&- Unique: Value must be unique across all records^- Indexed: Create an index for fast lookups
User {
id: +uuid // Auto-generated UUID
email: &string // Unique email
username: ^&string // Indexed and unique
}
Auto-generate support:
u32,u64- Auto-incrementing integersuuid- Random UUID generationtimestamp- Current timestamp
Directives
Directives are validation and configuration rules applied with the @ symbol:
Validation Directives
User {
email: string @email
age: u32 @min(0) @max(150)
password: string @length(8, 128)
website: string @url
name: string @length(1, 100)
}
Common enforced directives (a violation rejects the write with HTTP 422):
@email- Email format validation (string)@url- URL format validation (string)@min(n)- Minimum value — numeric fields only@max(n)- Maximum value — numeric fields only (not a string-length check; use@length)@length(min, max)- String length range (string)@pattern("…")/@regex("…")- Regex match (string)
Uniqueness is the & modifier, not a directive. (@min/@max on a string,
or any unrecognized directive such as @private/@unique, parses but is a
no-op — it enforces nothing.)
Computed Fields
User {
first_name: string
last_name: string
full_name: string @computed
}
Fulltext Search
Post {
title: string @fulltext
content: string @fulltext
}
Materialized Views
User {
post_count: u32 @materialized
}
Soft Delete
User {
id: +uuid
name: string
@soft_delete
}
Relations
ForgeDB supports several types of relations between models:
One-to-Many Relations
Use [ModelName] to define a collection:
User {
id: +uuid
posts: [Post]
}
Post {
id: +uuid
author: *User // Required reference back to User
}
Required References
Use *ModelName for a required foreign key:
Post {
id: +uuid
author: *User // Must reference a User
}
Optional References
Use ?ModelName for an optional foreign key:
Post {
id: +uuid
reviewer: ?User // May or may not reference a User
}
Many-to-Many Relations
Define bidirectional collections without foreign keys:
Post {
id: +uuid
tags: [Tag]
}
Tag {
id: +uuid
posts: [Post]
}
Composite Indexes
Define multi-field indexes at the model level:
User {
id: +uuid
first_name: string
last_name: string
city: string
state: string
@index(first_name, last_name)
@index(city, state)
}
Struct Definitions
Structs are fixed-size composite types that can only contain fixed-size fields:
struct Point {
x: f64
y: f64
}
struct Rectangle {
top_left: Point
bottom_right: Point
}
Model {
location: Point
bounds: ?Rectangle
}
Component References
Reference UI components and API routes:
User {
id: +uuid
name: string
posts: [Post]
// TSX component with all relations
profileCard: tsx://components/user/ProfileCard @relations(*)
// JSX component with specific relations
avatarView: jsx://components/user/AvatarView @relations(posts)
// API route
updateProfile: api://routes/user/update
}
Component protocols:
tsx://path- TypeScript + JSX componentjsx://path- JavaScript + JSX componentapi://path- API route handler
Relation inclusion:
@relations(*)- Include all relations@relations(field1, field2)- Include specific relations- No directive - No relations included
Usage Examples
Parsing a Schema File
use Parser;
// Parse a schema string
let input = r#"
User {
id: +uuid
email: &string
name: string
posts: [Post]
}
Post {
id: +uuid
title: string
author: *User
}
"#;
let mut parser = new.unwrap;
let schema = parser.parse.unwrap;
// Access models
assert_eq!;
assert_eq!;
assert_eq!;
Working with the AST
use Parser;
let input = r#"
User {
id: +uuid
email: &string @email
age: u32 @min(0) @max(150)
}
"#;
let mut parser = new.unwrap;
let schema = parser.parse.unwrap;
// Find a model
let user = schema.find_model.unwrap;
// Iterate fields
for field in &user.fields
Validating Relations
use Parser;
let input = r#"
User {
id: +uuid
posts: [Post]
}
Post {
id: +uuid
author: *User
}
"#;
let mut parser = new.unwrap;
let schema = parser.parse.unwrap;
// Validate all relations exist
assert!;
// Detect relation pairs
let relations = schema.detect_relations;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
Detecting Many-to-Many Relations
use Parser;
let input = r#"
Post {
id: +uuid
tags: [Tag]
}
Tag {
id: +uuid
posts: [Post]
}
"#;
let mut parser = new.unwrap;
let schema = parser.parse.unwrap;
// Detect M:N relations
let m2m = schema.detect_many_to_many_relations;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
Error Handling
use Parser;
// Invalid: duplicate field names
let input = r#"
User {
id: +uuid
email: string
email: string
}
"#;
let mut parser = new.unwrap;
let result = parser.parse;
assert!;
let error = result.unwrap_err;
assert!;
Disabling Validation
use Parser;
// Create parser without validation
let input = "User { InvalidFieldName: string }";
let mut parser = new_with_validation.unwrap;
let schema = parser.parse.unwrap;
AST Structure
Core Types
Schema
The root of the AST, containing all models and structs:
Methods:
find_model(&self, name: &str) -> Option<&Model>- Find a model by namefind_struct(&self, name: &str) -> Option<&Struct>- Find a struct by namevalidate_relations(&self) -> Result<(), String>- Validate all relationsvalidate_struct_references(&self) -> Result<(), String>- Validate struct referencesdetect_relations(&self) -> Vec<RelationPair>- Find 1:N relationshipsdetect_many_to_many_relations(&self) -> Vec<ManyToManyRelation>- Find M:N relationships
Model
Represents a database model:
Struct
Represents a fixed-size composite type:
Methods:
calculate_size(struct_def: &Struct, schema: &Schema) -> usize- Calculate total size with paddingcalculate_alignment(struct_def: &Struct, schema: &Schema) -> usize- Calculate alignment requirement
Field
Represents a model or struct field:
Methods:
has_constraint(&self, name: &str) -> bool- Check for a constraintget_constraint(&self, name: &str) -> Option<&Constraint>- Get a constraintis_nullable(&self) -> bool- Check if field is nullable
Type System
FieldType
Enum representing all possible field types:
Methods:
to_rust_type(&self) -> String- Convert to Rust type stringis_auto_incrementable(&self) -> bool- Check if can auto-incrementis_auto_generatable(&self) -> bool- Check if can auto-generateis_relation(&self) -> bool- Check if relation typeis_fixed_size(&self) -> bool- Check if fixed-sizestruct_name(&self) -> Option<&str>- Get struct name if struct typesize_in_bytes(&self, schema: &Schema) -> usize- Get size for fixed typesalignment(&self, schema: &Schema) -> usize- Get alignment requirementsupports_range_queries(&self) -> bool- Check if ordered typedefault_index_type(&self) -> IndexType- Get default index type
RelationType
Enum for relation types:
Methods:
target_model(&self) -> &str- Get target model nameis_one_to_many(&self) -> bool- Check if one-to-manyis_reference(&self) -> bool- Check if foreign key referenceis_many_to_many(&self) -> bool- Check if many-to-many
IndexType
Index type for fields:
Constraints
Constraint
Represents a validation directive:
Methods:
new(name: String) -> Self- Create constraint without parameterswith_param(self, param: ConstraintParam) -> Self- Add a parameter
ConstraintParam
Constraint parameter value:
Component Integration
ComponentReference
Reference to a UI component or API route:
ComponentProtocol
RelationInclusion
Composite Indexes
CompositeIndex
Multi-field index definition:
Relation Detection
RelationPair
One-to-many relationship:
ManyToManyRelation
Many-to-many relationship:
Traversing the AST
use Parser;
let mut parser = new.unwrap;
let schema = parser.parse.unwrap;
// Iterate all models
for model in &schema.models
// Iterate all structs
for struct_def in &schema.structs
API Reference
Parser Type
The main parser type for parsing schema strings:
Constructor Methods
-
Parser::new(input: &str) -> Result<Self, String>- Create a new parser with validation enabled
- Returns error if lexing fails
-
Parser::new_with_validation(input: &str, use_validation: bool) -> Result<Self, String>- Create a parser with optional validation
- Set
use_validationtofalseto disable name validation
Parsing Methods
parser.parse(&mut self) -> Result<Schema, String>- Parse the input and return the AST
- Returns error if parsing fails with detailed error message
Lexer Type
The lexer tokenizes schema input:
Constructor
Lexer::new(input: &str) -> Self- Create a new lexer for the input string
Methods
-
lexer.next_token(&mut self) -> Result<Token, String>- Get the next token
- Returns error for unexpected characters
-
lexer.next_token_with_pos(&mut self) -> Result<TokenWithPos, String>- Get next token with position information
-
lexer.tokenize(&mut self) -> Result<Vec<Token>, String>- Tokenize entire input
- Returns all tokens including EOF
-
lexer.tokenize_with_pos(&mut self) -> Result<Vec<TokenWithPos>, String>- Tokenize with position information for error reporting
Token Type
Token types recognized by the lexer:
Error Handling
Parse Errors
The parser provides detailed error messages with context:
use Parser;
let input = r#"
User {
id: +string // Invalid: string cannot be auto-generated
}
"#;
let mut parser = new.unwrap;
match parser.parse
Error Categories
Lexer Errors
- Unexpected character:
"Unexpected character 'x' at line 5, column 10" - Invalid number format: Fails to parse numeric literal
Parser Errors
- Missing token:
"Expected '{', found 'identifier'" - Empty model:
"Model 'User' must have at least one field" - Empty schema:
"Schema must contain at least one model" - Invalid field type:
"Unknown type 'int32'"
Validation Errors
- Duplicate field:
"Duplicate field name 'email' in model 'User'" - Duplicate model:
"Duplicate model name 'User'" - Invalid field name:
"Field name 'UserName' must be snake_case. Suggested: 'user_name'" - Invalid model name:
"Model name 'user_model' must be PascalCase. Suggested: 'UserModel'" - Auto-generate misuse:
"Auto-generate symbol '+' cannot be used with type 'string'" - Undefined reference:
"Model 'Post' field 'author' references undefined model 'User'" - Variable-length in struct:
"Struct 'Data' field 'text' contains variable-length type" - Composite index error:
"Field 'email' in @index directive not found in model 'User'"
Error Recovery
The parser does not currently support error recovery - it stops at the first error encountered. This ensures clean error messages without cascading errors.
Position Information
Errors include line and column information when available:
use Lexer;
let mut lexer = new;
match lexer.tokenize
Testing
Running Parser Tests
# Run all parser tests
# Run with output
# Run specific test
Integration Tests
The parser has extensive integration tests in /tests/parser_tests.rs covering:
- Basic model parsing
- All primitive types
- Field modifiers (auto-generate, unique, indexed)
- Relations (one-to-many, required reference, optional reference)
- Constraints and directives
- Composite indexes
- Struct definitions
- Component references
- Error cases and validation
- Name validation (PascalCase for models, snake_case for fields)
Test Coverage
The parser tests cover:
- ✅ Simple and complex model definitions
- ✅ All primitive and fixed-size types
- ✅ Field modifiers and combinations
- ✅ All relation types
- ✅ Constraint parsing with parameters
- ✅ Composite index definitions
- ✅ Struct parsing and validation
- ✅ Component integration (TSX/JSX/API)
- ✅ Duplicate detection (models, fields)
- ✅ Name validation (PascalCase, snake_case)
- ✅ Invalid auto-generate usage
- ✅ Undefined reference detection
- ✅ Error message quality
Example Test
use Parser;
use *;
Installation
Add this to your Cargo.toml:
[]
= "0.1"
Or with path dependency for local development:
[]
= { = "../parser" }
Dependencies
forgedb-validation- Validation utilities for names and constraints
Contributing
This crate is part of the ForgeDB project. For contribution guidelines, see the main repository.
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT License (LICENSE-MIT)
at your option.