Platform/Rust Integration
This walkthrough blends Topaz with the Rust/Actix ecosystem. Examples show interop glue so you can deploy Topaz logic inside existing Rust services.
Build your first complete project with Topaz! We'll create a Todo Management API while experiencing all of Topaz's core features. 🚀
🎯 Project Overview
What we're building
Personal Todo Management REST API
Technologies used
Topaz web server, JSON processing, file storage, error handling
What you'll learn
Project structure, routing, data modeling, testing
📁 Project Setup
1. Create New Project
# Create new Topaz project
topaz new TodoAPI --template=web-api
cd TodoAPI
# Check project structure
tree .
Generated structure:
src/
├─ main.tpz # Main entry point
├─ models/ # Data models
├─ handlers/ # Request handlers
└─ utils/ # Utility functions
tests/ # Test files
data/ # Data storage
topaz.toml # Project configuration
README.md
2. Add Dependencies
# topaz.toml
[package]
name = "TodoAPI"
version = "0.1.0"
description = "REST API for personal todo management"
[dependencies]
web = "2.0" # Web server framework
json = "1.5" # JSON processing
uuid = "1.0" # Unique ID generation
chrono = "0.9" # Date/time handling
serde = "2.1" # Serialization/deserialization
[dev-dependencies]
test-framework = "1.0"
http-client = "0.8"
🏗️ Data Modeling
Todo Model Definition
// src/models/todo.tpz
use chrono::{DateTime, Utc}
use uuid::Uuid
use serde::{Serialize, Deserialize}
#[derive(Serialize, Deserialize, Clone, Debug)]
struct Todo {
id: Uuid,
title: string,
description: Option<string>,
completed: bool,
priority: Priority,
createdAt: DateTime<Utc>,
completedAt: Option<DateTime<Utc>>,
tags: Array<string>
}
#[derive(Serialize, Deserialize, Clone, Debug)]
enum Priority {
Low,
Normal,
High,
Urgent
}
impl Todo {
// Create new todo
function new(
title: string,
description: Option<string> = None,
priority: Priority = Priority::Normal
) -> Todo {
return Todo {
id: Uuid::new_v4(),
title,
description,
completed: false,
priority,
createdAt: Utc::now(),
completedAt: None,
tags: []
}
}
// Mark todo as completed
function complete(&mut self) {
self.completed = true
self.completedAt = Some(Utc::now())
}
// Add tag
function addTag(&mut self, newTag: string) {
if !self.tags.contains(&newTag) {
self.tags.push(newTag)
}
}
// Get priority score for sorting
function priorityScore(&self) -> int {
match self.priority {
case Priority::Urgent => 4
case Priority::High => 3
case Priority::Normal => 2
case Priority::Low => 1
}
}
} test {
// Todo creation test
let todo = Todo::new("Learn Topaz".to_string(), None, Priority::High)
assert todo.title == "Learn Topaz"
assert todo.completed == false
assert todo.priority == Priority::High
// Completion test
let mut todo = Todo::new("Test".to_string(), None, Priority::Normal)
todo.complete()
assert todo.completed == true
assert todo.completedAt.is_some() == true
// Tag addition test
let mut todo = Todo::new("Project".to_string(), None, Priority::Normal)
todo.addTag("development".to_string())
todo.addTag("learning".to_string())
todo.addTag("development".to_string()) // Duplicate attempt
assert todo.tags.len() == 2
assert todo.tags.contains(&"development".to_string())
assert todo.tags.contains(&"learning".to_string())
}
Request/Response Models
// src/models/dto.tpz
use serde::{Serialize, Deserialize}
use uuid::Uuid
#[derive(Deserialize)]
struct CreateTodoRequest {
title: string,
description: Option<string>,
priority: Option<Priority>,
tags: Option<Array<string>>
}
#[derive(Deserialize)]
struct UpdateTodoRequest {
title: Option<string>,
description: Option<string>,
priority: Option<Priority>,
tags: Option<Array<string>>
}
#[derive(Serialize)]
struct TodoResponse {
id: Uuid,
title: string,
description: Option<string>,
completed: bool,
priority: Priority,
createdAt: string,
completedAt: Option<string>,
tags: Array<string>
}
impl From<Todo> for TodoResponse {
function from(todo: Todo) -> TodoResponse {
return TodoResponse {
id: todo.id,
title: todo.title,
description: todo.description,
completed: todo.completed,
priority: todo.priority,
createdAt: todo.createdAt.format("%Y-%m-%d %H:%M:%S").to_string(),
completedAt: todo.completedAt.map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string()),
tags: todo.tags
}
}
}
#[derive(Serialize)]
struct ApiResponse<T> {
success: bool,
data: Option<T>,
message: string
}
impl<T> ApiResponse<T> {
function success(data: T) -> ApiResponse<T> {
return ApiResponse {
success: true,
data: Some(data),
message: "Success".to_string()
}
}
function error(message: string) -> ApiResponse<T> {
return ApiResponse {
success: false,
data: None,
message
}
}
}
💾 Data Storage
Simple File-based Storage
// src/storage/file_store.tpz
use std::fs
use std::path::Path
use uuid::Uuid
use crate::models::Todo
struct FileStore {
dataPath: string
}
impl FileStore {
function new(dataPath: string) -> Result<FileStore, string> {
// Create data directory
if !Path::new(&dataPath).exists() {
fs::create_dir_all(&dataPath)
.map_err(|e| "Failed to create data directory: {e}".to_string())?
}
return Ok(FileStore { dataPath })
}
function filePath(&self) -> string {
return format!("{}/todos.json", self.dataPath)
}
// Load all todos
function getAll(&self) -> Result<Array<Todo>, string> {
let path = self.filePath()
if !Path::new(&path).exists() {
return Ok([])
}
let content = fs::read_to_string(&path)
.map_err(|e| "Failed to read file: {e}".to_string())?
let todos: Array<Todo> = serde_json::from_str(&content)
.map_err(|e| "Failed to parse JSON: {e}".to_string())?
return Ok(todos)
}
// Save all todos
function saveAll(&self, todos: &Array<Todo>) -> Result<(), string> {
let jsonString = serde_json::to_string_pretty(todos)
.map_err(|e| "Failed to serialize JSON: {e}".to_string())?
fs::write(self.filePath(), jsonString)
.map_err(|e| "Failed to write file: {e}".to_string())?
return Ok(())
}
// Add todo
function add(&self, todo: Todo) -> Result<Todo, string> {
let mut todos = self.getAll()?
todos.push(todo.clone())
self.saveAll(&todos)?
return Ok(todo)
}
// Find todo by ID
function findById(&self, id: Uuid) -> Result<Option<Todo>, string> {
let todos = self.getAll()?
let foundTodo = todos.iter().find(|todo| todo.id == id).cloned()
return Ok(foundTodo)
}
// Update todo
function update(&self, id: Uuid, updatedTodo: Todo) -> Result<Option<Todo>, string> {
let mut todos = self.getAll()?
return match todos.iter().position(|todo| todo.id == id) {
case Some(index) => {
todos[index] = updatedTodo.clone()
self.saveAll(&todos)?
Ok(Some(updatedTodo))
}
case None => Ok(None)
}
}
// Delete todo
function delete(&self, id: Uuid) -> Result<bool, string> {
let mut todos = self.getAll()?
return match todos.iter().position(|todo| todo.id == id) {
case Some(index) => {
todos.remove(index)
self.saveAll(&todos)?
Ok(true)
}
case None => Ok(false)
}
}
// Search todos with filter
function search<F>(&self, filter: F) -> Result<Array<Todo>, string>
where
F: Fn(&Todo) -> bool
{
let todos = self.getAll()?
let result = todos.into_iter().filter(filter).collect()
return Ok(result)
}
} test {
use tempfile::tempdir
// Test in temporary directory
let tempDir = tempdir().unwrap()
let store = FileStore::new(tempDir.path().to_str().unwrap().to_string()).unwrap()
// Add todo test
let newTodo = Todo::new("Test Todo".to_string(), None, Priority::Normal)
let addedTodo = store.add(newTodo.clone()).unwrap()
assert addedTodo.id == newTodo.id
// Find todo test
let foundTodo = store.findById(newTodo.id).unwrap()
assert foundTodo.is_some()
assert foundTodo.unwrap().title == "Test Todo"
// Get all todos test
let allTodos = store.getAll().unwrap()
assert allTodos.len() == 1
}
🌐 Web Server and Routing
Main Server Setup
// src/main.tpz
mod models
mod storage
mod handlers
use web::{App, HttpServer, middleware}
use storage::FileStore
use handlers::*
#[tokio::main]
function main() -> Result<(), Box<dyn std::error::Error>> {
// Setup logging
env_logger::init()
// Initialize data store
let store = FileStore::new("./data".to_string())?
println!("🚀 Starting Todo Management API server...")
println!("📍 Server address: http://localhost:8080")
println!("📋 API docs: http://localhost:8080/docs")
// Start HTTP server
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(store.clone()))
.wrap(middleware::Logger::default())
.wrap(middleware::DefaultHeaders::new()
.header("Content-Type", "application/json; charset=utf-8"))
.service(
web::scope("/api/v1")
.route("/todos", web::get().to(getAllTodos))
.route("/todos", web::post().to(createTodo))
.route("/todos/{id}", web::get().to(getTodo))
.route("/todos/{id}", web::put().to(updateTodo))
.route("/todos/{id}", web::delete().to(deleteTodo))
.route("/todos/{id}/complete", web::patch().to(completeTodo))
.route("/todos/search", web::get().to(searchTodos))
)
.route("/health", web::get().to(healthCheck))
.route("/docs", web::get().to(apiDocs))
})
.bind("127.0.0.1:8080")?
.run()?
Ok(())
}
Request Handlers
// src/handlers/todo_handlers.tpz
use web::{web, HttpRequest, HttpResponse, Result as WebResult}
use uuid::Uuid
use crate::models::{Todo, CreateTodoRequest, UpdateTodoRequest, TodoResponse, ApiResponse}
use crate::storage::FileStore
// Get all todos
function getAllTodos(
store: web::Data<FileStore>
) -> WebResult<HttpResponse> {
match store.getAll() {
case Ok(todos) => {
let responseTodos: Array<TodoResponse> = todos
.into_iter()
.map(|todo| TodoResponse::from(todo))
.collect()
return Ok(HttpResponse::Ok()
.json(ApiResponse::success(responseTodos)))
}
case Err(error) => {
return Ok(HttpResponse::InternalServerError()
.json(ApiResponse::<()>::error(error)))
}
}
}
// Create new todo
function createTodo(
request: web::Json<CreateTodoRequest>,
store: web::Data<FileStore>
) -> WebResult<HttpResponse> {
let mut newTodo = Todo::new(
request.title.clone(),
request.description.clone(),
request.priority.unwrap_or(Priority::Normal)
)
// Add tags if provided
match &request.tags {
case Some(tagList) => {
for tag in tagList {
newTodo.addTag(tag.clone())
}
}
case None => {}
}
match store.add(newTodo) {
case Ok(createdTodo) => {
return Ok(HttpResponse::Created()
.json(ApiResponse::success(TodoResponse::from(createdTodo))))
}
case Err(error) => {
return Ok(HttpResponse::InternalServerError()
.json(ApiResponse::<()>::error(error)))
}
}
}
// Get specific todo
function getTodo(
path: web::Path<Uuid>,
store: web::Data<FileStore>
) -> WebResult<HttpResponse> {
let id = path.into_inner()
match store.findById(id) {
case Ok(Some(todo)) => {
return Ok(HttpResponse::Ok()
.json(ApiResponse::success(TodoResponse::from(todo))))
}
case Ok(None) => {
return Ok(HttpResponse::NotFound()
.json(ApiResponse::<()>::error("Todo not found".to_string())))
}
case Err(error) => {
return Ok(HttpResponse::InternalServerError()
.json(ApiResponse::<()>::error(error)))
}
}
}
// Update todo
function updateTodo(
path: web::Path<Uuid>,
request: web::Json<UpdateTodoRequest>,
store: web::Data<FileStore>
) -> WebResult<HttpResponse> {
let id = path.into_inner()
match store.findById(id) {
case Ok(Some(mut existingTodo)) => {
// Update only requested fields
match &request.title {
case Some(title) => {
existingTodo.title = title.clone()
}
case None => {}
}
match &request.description {
case Some(description) => {
existingTodo.description = Some(description.clone())
}
case None => {}
}
match &request.priority {
case Some(priority) => {
existingTodo.priority = priority.clone()
}
case None => {}
}
match &request.tags {
case Some(tagList) => {
existingTodo.tags = tagList.clone()
}
case None => {}
}
match store.update(id, existingTodo) {
case Ok(Some(updatedTodo)) => {
return Ok(HttpResponse::Ok()
.json(ApiResponse::success(TodoResponse::from(updatedTodo))))
}
case Ok(None) => {
return Ok(HttpResponse::NotFound()
.json(ApiResponse::<()>::error("Todo not found".to_string())))
}
case Err(error) => {
return Ok(HttpResponse::InternalServerError()
.json(ApiResponse::<()>::error(error)))
}
}
}
case Ok(None) => {
return Ok(HttpResponse::NotFound()
.json(ApiResponse::<()>::error("Todo not found".to_string())))
}
case Err(error) => {
return Ok(HttpResponse::InternalServerError()
.json(ApiResponse::<()>::error(error)))
}
}
}
// Delete todo
function deleteTodo(
path: web::Path<Uuid>,
store: web::Data<FileStore>
) -> WebResult<HttpResponse> {
let id = path.into_inner()
match store.delete(id) {
case Ok(true) => {
return Ok(HttpResponse::Ok()
.json(ApiResponse::success("Todo deleted successfully".to_string())))
}
case Ok(false) => {
return Ok(HttpResponse::NotFound()
.json(ApiResponse::<()>::error("Todo not found".to_string())))
}
case Err(error) => {
return Ok(HttpResponse::InternalServerError()
.json(ApiResponse::<()>::error(error)))
}
}
}
// Complete todo
function completeTodo(
path: web::Path<Uuid>,
store: web::Data<FileStore>
) -> WebResult<HttpResponse> {
let id = path.into_inner()
match store.findById(id) {
case Ok(Some(mut todo)) => {
todo.complete()
match store.update(id, todo) {
case Ok(Some(completedTodo)) => {
return Ok(HttpResponse::Ok()
.json(ApiResponse::success(TodoResponse::from(completedTodo))))
}
case Err(error) => {
return Ok(HttpResponse::InternalServerError()
.json(ApiResponse::<()>::error(error)))
}
case _ => {
return Ok(HttpResponse::InternalServerError()
.json(ApiResponse::<()>::error("Failed to complete todo".to_string())))
}
}
}
case Ok(None) => {
return Ok(HttpResponse::NotFound()
.json(ApiResponse::<()>::error("Todo not found".to_string())))
}
case Err(error) => {
return Ok(HttpResponse::InternalServerError()
.json(ApiResponse::<()>::error(error)))
}
}
}
// Search todos (using query parameters)
function searchTodos(
request: HttpRequest,
store: web::Data<FileStore>
) -> WebResult<HttpResponse> {
let query = web::Query::<HashMap<String, String>>::from_query(request.query_string())
.map_err(|_| HttpResponse::BadRequest().json(ApiResponse::<()>::error("Invalid query parameters".to_string())))?
let todos = match store.getAll() {
case Ok(list) => list
case Err(error) => {
return Ok(HttpResponse::InternalServerError()
.json(ApiResponse::<()>::error(error)))
}
}
let mut filteredTodos = todos
// Filter by completion status
match query.get("completed") {
case Some(completedStr) => {
let completedStatus = completedStr.parse::<bool>().unwrap_or(false)
filteredTodos = filteredTodos.into_iter()
.filter(|todo| todo.completed == completedStatus)
.collect()
}
case None => {}
}
// Filter by priority
match query.get("priority") {
case Some(priorityStr) => {
match priorityStr.parse::<Priority>() {
case Ok(priority) => {
filteredTodos = filteredTodos.into_iter()
.filter(|todo| todo.priority == priority)
.collect()
}
case Err(_) => {}
}
}
case None => {}
}
// Filter by tag
match query.get("tag") {
case Some(tag) => {
filteredTodos = filteredTodos.into_iter()
.filter(|todo| todo.tags.contains(tag))
.collect()
}
case None => {}
}
// Search in title
match query.get("search") {
case Some(searchTerm) => {
let lowercaseSearch = searchTerm.to_lowercase()
filteredTodos = filteredTodos.into_iter()
.filter(|todo| todo.title.to_lowercase().contains(&lowercaseSearch))
.collect()
}
case None => {}
}
// Sort by priority
filteredTodos.sort_by(|a, b| b.priorityScore().cmp(&a.priorityScore()))
let responseTodos: Array<TodoResponse> = filteredTodos
.into_iter()
.map(|todo| TodoResponse::from(todo))
.collect()
return Ok(HttpResponse::Ok()
.json(ApiResponse::success(responseTodos)))
}
// Health check
function healthCheck() -> WebResult<HttpResponse> {
return Ok(HttpResponse::Ok()
.json(json!({
"status": "healthy",
"timestamp": chrono::Utc::now().to_rfc3339(),
"version": "0.1.0"
})))
}
// API documentation
function apiDocs() -> WebResult<HttpResponse> {
let docs = r#"
<!DOCTYPE html>
<html>
<head>
<title>Todo Management API Documentation</title>
<meta charset="utf-8">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 40px; }
.endpoint { background: #f8f9fa; padding: 15px; margin: 10px 0; border-radius: 5px; }
.method { font-weight: bold; padding: 3px 8px; border-radius: 3px; color: white; }
.get { background: #28a745; }
.post { background: #007bff; }
.put { background: #ffc107; color: black; }
.delete { background: #dc3545; }
.patch { background: #6f42c1; }
</style>
</head>
<body>
<h1>🚀 Todo Management API Documentation</h1>
<div class="endpoint">
<span class="method get">GET</span>
<strong>/api/v1/todos</strong>
<p>Get all todos.</p>
</div>
<div class="endpoint">
<span class="method post">POST</span>
<strong>/api/v1/todos</strong>
<p>Create a new todo.</p>
<pre>{ "title": "string", "description": "string?", "priority": "enum?", "tags": "string[]?" }</pre>
</div>
<div class="endpoint">
<span class="method get">GET</span>
<strong>/api/v1/todos/{id}</strong>
<p>Get a specific todo.</p>
</div>
<div class="endpoint">
<span class="method put">PUT</span>
<strong>/api/v1/todos/{id}</strong>
<p>Update a todo.</p>
</div>
<div class="endpoint">
<span class="method delete">DELETE</span>
<strong>/api/v1/todos/{id}</strong>
<p>Delete a todo.</p>
</div>
<div class="endpoint">
<span class="method patch">PATCH</span>
<strong>/api/v1/todos/{id}/complete</strong>
<p>Mark a todo as completed.</p>
</div>
<div class="endpoint">
<span class="method get">GET</span>
<strong>/api/v1/todos/search</strong>
<p>Search todos. Query parameters: completed, priority, tag, search</p>
</div>
<h2>🏥 Health Check</h2>
<div class="endpoint">
<span class="method get">GET</span>
<strong>/health</strong>
<p>Check server status.</p>
</div>
</body>
</html>
"#;
return Ok(HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(docs))
}
🧪 Writing Tests
Integration Tests
// tests/integration_test.tpz
use actix_web::{test, App}
use serde_json::json
use tempfile::tempdir
use TodoAPI::*
#[actix_rt::test]
function todo_crud_test() -> Result<(), Box<dyn std::error::Error>> {
// Setup temporary storage
let tempDir = tempdir().unwrap()
let store = FileStore::new(tempDir.path().to_str().unwrap().to_string()).unwrap()
let mut app = test::init_service(
App::new()
.app_data(web::Data::new(store))
.service(
web::scope("/api/v1")
.route("/todos", web::get().to(getAllTodos))
.route("/todos", web::post().to(createTodo))
.route("/todos/{id}", web::get().to(getTodo))
.route("/todos/{id}", web::put().to(updateTodo))
.route("/todos/{id}", web::delete().to(deleteTodo))
)
)?
// 1. Create todo test
let newTodoRequest = json!({
"title": "Complete Topaz Project",
"description": "Finish the first Topaz project",
"priority": "High",
"tags": ["development", "learning"]
})
let req = test::TestRequest::post()
.uri("/api/v1/todos")
.set_json(&newTodoRequest)
.to_request()
let resp = test::call_service(&mut app, req)?
assert!(resp.status().is_success())
let respBody: serde_json::Value = test::read_body_json(resp)?
assert!(respBody["success"].as_bool().unwrap())
let createdTodo = &respBody["data"]
let todoId = createdTodo["id"].as_str().unwrap()
// 2. Get todo test
let req = test::TestRequest::get()
.uri(&format!("/api/v1/todos/{}", todoId))
.to_request()
let resp = test::call_service(&mut app, req)?
assert!(resp.status().is_success())
let respBody: serde_json::Value = test::read_body_json(resp)?
let retrievedTodo = &respBody["data"]
assert_eq!(retrievedTodo["title"], "Complete Topaz Project")
// 3. Get all todos test
let req = test::TestRequest::get()
.uri("/api/v1/todos")
.to_request()
let resp = test::call_service(&mut app, req)?
assert!(resp.status().is_success())
let respBody: serde_json::Value = test::read_body_json(resp)?
let todoList = respBody["data"].as_array().unwrap()
assert_eq!(todoList.len(), 1)
// 4. Update todo test
let updateRequest = json!({
"title": "Complete Topaz Project (Updated)",
"priority": "Urgent"
})
let req = test::TestRequest::put()
.uri(&format!("/api/v1/todos/{}", todoId))
.set_json(&updateRequest)
.to_request()
let resp = test::call_service(&mut app, req)?
assert!(resp.status().is_success())
// 5. Delete todo test
let req = test::TestRequest::delete()
.uri(&format!("/api/v1/todos/{}", todoId))
.to_request()
let resp = test::call_service(&mut app, req)?
assert!(resp.status().is_success())
// 6. Verify deletion
let req = test::TestRequest::get()
.uri(&format!("/api/v1/todos/{}", todoId))
.to_request()
let resp = test::call_service(&mut app, req)?
assert_eq!(resp.status(), 404)
Ok(())
}
#[actix_rt::test]
function todo_search_test() -> Result<(), Box<dyn std::error::Error>> {
// Test data setup and search functionality testing
// ... (search-related test code)
Ok(())
}
🚀 Running and Testing
Run Project
# Install dependencies
topaz build
# Run development server
topaz run
# Or run in release mode
topaz run --release
API Testing
# Health check
curl http://localhost:8080/health
# Create new todo
curl -X POST http://localhost:8080/api/v1/todos \
-H "Content-Type: application/json" \
-d '{
"title": "Learn Topaz",
"description": "Learn all features of Topaz language",
"priority": "High",
"tags": ["learning", "programming"]
}'
# Get all todos
curl http://localhost:8080/api/v1/todos
# Search incomplete todos
curl "http://localhost:8080/api/v1/todos/search?completed=false"
# Search high priority todos
curl "http://localhost:8080/api/v1/todos/search?priority=High"
Run Tests
# Run all tests
topaz test
# Run specific test
topaz test integration_test
# Check test coverage
topaz test --coverage
🎯 Next Steps
Congratulations! You've completed your first complete Topaz project! 🎉
Additional features to implement:
- Database Integration: PostgreSQL, MongoDB, etc.
- User Authentication: JWT token-based authentication
- Real-time Updates: Real-time todo synchronization via WebSocket
- File Upload: File attachment feature for todos
- Email Notifications: Deadline reminder notifications
- Docker Deployment: Containerization and cloud deployment
Core Topaz concepts you've learned:
- ✅ Project structure design
- ✅ Data modeling and type system
- ✅ Error handling (Result/Option types)
- ✅ Web server and REST API construction
- ✅ File-based data storage
- ✅ Pattern matching utilization
- ✅ Test-driven development
- ✅ Multi-language identifiers and natural code writing
You're now ready to tackle more complex Topaz projects! 🚀