mirror of https://github.com/buster-so/buster.git
1481 lines
45 KiB
Markdown
1481 lines
45 KiB
Markdown
# Buster API Development Guide
|
|
|
|
## Documentation
|
|
The project's detailed documentation can be found in the `/documentation` directory. This directory contains the following files with comprehensive information on various aspects of the codebase:
|
|
|
|
- `handlers.mdc` - Documentation for writing and using handlers
|
|
- `libs.mdc` - Guidelines for library construction and organization
|
|
- `prds.mdc` - Product Requirements Document guidelines
|
|
- `rest.mdc` - REST API formatting rules and patterns
|
|
- `testing.mdc` - Testing standards, utilities, and best practices
|
|
- `tools.mdc` - Documentation for building tools
|
|
- `websockets.mdc` - WebSocket API formatting rules
|
|
|
|
When working on the codebase, please refer to these documentation files for detailed guidance on implementation patterns, code organization, and best practices.
|
|
|
|
## Build Commands
|
|
- `make dev` - Start the development environment
|
|
- `make stop` - Stop the development environment
|
|
- `cargo test -- --test-threads=1 --nocapture` - Run all tests
|
|
- `cargo test <test_name> -- --nocapture` - Run a specific test
|
|
- `cargo clippy` - Run the linter
|
|
- `cargo build` - Build the project
|
|
- `cargo watch -x run` - Watch for changes and run
|
|
|
|
## Code Style Guidelines
|
|
- **Error Handling**: Use `anyhow::Result` for functions that can fail. Create specialized errors with `thiserror`.
|
|
- **Naming**: Use snake_case for variables/functions, CamelCase for types/structs.
|
|
- **Types**: Put shared types in `types/`, route-specific types in route files.
|
|
- **Organization**: Follow the repo structure in README.md.
|
|
- **Imports**: Group imports by std lib, external crates, and internal modules.
|
|
- **Testing**: Write tests directly in route files. Use `tokio::test` attribute for async tests.
|
|
- **Documentation**: Document public APIs. Use rustdoc-style comments.
|
|
- **Async**: Use async/await with Tokio. Handle futures properly.
|
|
- **Validation**: Validate inputs with proper error messages.
|
|
- **Security**: Never log secrets or sensitive data.
|
|
- **Dependencies**: All dependencies must be inherited from the workspace Cargo.toml using `{ workspace = true }`. Never specify library-specific dependency versions to ensure consistent dependency versions across the entire project.
|
|
|
|
# Handler Rules and Best Practices
|
|
|
|
## Overview
|
|
Handlers are the core business logic components that implement functionality used by both REST and WebSocket endpoints. They are also often used by the CLI package. This document outlines the structure, patterns, and best practices for working with handlers.
|
|
|
|
## File Structure
|
|
- `libs/handlers/src/`
|
|
- `[domain]/` - Domain-specific modules (e.g., messages, chats, files, metrics)
|
|
- `mod.rs` - Re-exports handlers and types
|
|
- `types.rs` - Domain-specific data structures
|
|
- `*_handler.rs` - Individual handler implementations
|
|
- `helpers/` - Helper functions and utilities for handlers
|
|
|
|
## Naming Conventions
|
|
- Handler files should be named with the pattern: `[action]_[resource]_handler.rs`
|
|
- Example: `get_chat_handler.rs`, `delete_message_handler.rs`
|
|
- Handler functions should follow the same pattern: `[action]_[resource]_handler`
|
|
- Example: `get_chat_handler()`, `delete_message_handler()`
|
|
- Type definitions should be clear and descriptive
|
|
- Request types: `[Action][Resource]Request`
|
|
- Response types: `[Action][Resource]Response`
|
|
|
|
## Handler Implementation Guidelines
|
|
|
|
### Function Signatures
|
|
```rust
|
|
pub async fn action_resource_handler(
|
|
// Parameters typically include:
|
|
request: ActionResourceRequest, // For REST/WS request data
|
|
user: User, // For authenticated user context
|
|
// Other contextual parameters as needed
|
|
) -> Result<ActionResourceResponse> {
|
|
// Implementation
|
|
}
|
|
```
|
|
|
|
### Error Handling
|
|
- Use `anyhow::Result<T>` for return types
|
|
- Provide descriptive error messages with context
|
|
- Handle specific error cases appropriately
|
|
- Log errors with relevant context
|
|
- Example:
|
|
```rust
|
|
match operation() {
|
|
Ok(result) => Ok(result),
|
|
Err(diesel::NotFound) => Err(anyhow!("Resource not found")),
|
|
Err(e) => {
|
|
tracing::error!("Operation failed: {}", e);
|
|
Err(anyhow!("Operation failed: {}", e))
|
|
}
|
|
}
|
|
```
|
|
|
|
### Database Operations
|
|
- Use the connection pool: `get_pg_pool().get().await?`
|
|
- Run concurrent operations when possible
|
|
- Use transactions for related operations
|
|
- Handle database-specific errors appropriately
|
|
- Example:
|
|
```rust
|
|
let pool = get_pg_pool();
|
|
let mut conn = pool.get().await?;
|
|
|
|
diesel::update(table)
|
|
.filter(conditions)
|
|
.set(values)
|
|
.execute(&mut conn)
|
|
.await?
|
|
```
|
|
|
|
### Concurrency
|
|
- Use `tokio::spawn` for concurrent operations
|
|
- Use `futures::try_join_all` for parallel processing
|
|
- Be mindful of connection pool limits
|
|
- Example:
|
|
```rust
|
|
let thread_future = tokio::spawn(async move {
|
|
// Database operation 1
|
|
});
|
|
|
|
let messages_future = tokio::spawn(async move {
|
|
// Database operation 2
|
|
});
|
|
|
|
let (thread_result, messages_result) = tokio::join!(thread_future, messages_future);
|
|
```
|
|
|
|
### Logging
|
|
- Use structured logging with `tracing`
|
|
- Include relevant context in log messages
|
|
- Log at appropriate levels (info, warn, error)
|
|
- Example:
|
|
```rust
|
|
tracing::info!(
|
|
resource_id = %id,
|
|
user_id = %user.id,
|
|
"Processing resource action"
|
|
);
|
|
```
|
|
|
|
### Type Definitions
|
|
- Use `serde` for serialization/deserialization
|
|
- Define clear, reusable types
|
|
- Use appropriate validation
|
|
- Example:
|
|
```rust
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct ResourceRequest {
|
|
pub id: Uuid,
|
|
pub name: String,
|
|
#[serde(default)]
|
|
pub options: Vec<String>,
|
|
}
|
|
```
|
|
|
|
## Integration with REST and WebSocket APIs
|
|
- Handlers should be independent of transport mechanism
|
|
- Same handler can be used by both REST and WebSocket endpoints
|
|
- Handlers should focus on business logic, not HTTP/WebSocket specifics
|
|
- Example:
|
|
```rust
|
|
// In REST route
|
|
pub async fn rest_endpoint(
|
|
Json(payload): Json<HandlerRequest>,
|
|
user: User,
|
|
) -> Result<Json<HandlerResponse>, AppError> {
|
|
let result = handler::action_resource_handler(payload, user).await?;
|
|
Ok(Json(result))
|
|
}
|
|
|
|
// In WebSocket handler
|
|
async fn ws_message_handler(message: WsMessage, user: User) -> Result<WsResponse> {
|
|
let payload: HandlerRequest = serde_json::from_str(&message.payload)?;
|
|
let result = handler::action_resource_handler(payload, user).await?;
|
|
Ok(WsResponse::new(result))
|
|
}
|
|
```
|
|
|
|
## CLI Integration
|
|
- Handler types should be reusable in CLI commands
|
|
- CLI commands should use the same handlers as the API when possible
|
|
- Example:
|
|
```rust
|
|
// In CLI command
|
|
pub fn cli_command(args: &ArgMatches) -> Result<()> {
|
|
let request = HandlerRequest {
|
|
// Parse from args
|
|
};
|
|
|
|
let result = tokio::runtime::Runtime::new()?.block_on(async {
|
|
handler::action_resource_handler(request, mock_user()).await
|
|
})?;
|
|
|
|
println!("{}", serde_json::to_string_pretty(&result)?);
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
## Testing
|
|
- Write unit tests for handlers
|
|
- Mock database and external dependencies
|
|
- Test error cases and edge conditions
|
|
- Example:
|
|
```rust
|
|
#[tokio::test]
|
|
async fn test_action_resource_handler() {
|
|
// Setup test data
|
|
let request = HandlerRequest { /* ... */ };
|
|
let user = mock_user();
|
|
|
|
// Call handler
|
|
let result = action_resource_handler(request, user).await;
|
|
|
|
// Assert expectations
|
|
assert!(result.is_ok());
|
|
let response = result.unwrap();
|
|
assert_eq!(response.field, expected_value);
|
|
}
|
|
```
|
|
|
|
## Common Patterns
|
|
- Retrieve data from database
|
|
- Process and transform data
|
|
- Interact with external services
|
|
- Return structured response
|
|
- Handle errors and edge cases
|
|
- Log relevant information
|
|
|
|
# Global Rules and Project Structure
|
|
|
|
## Project Overview
|
|
This is a Rust web server project built with Axum, focusing on high performance, safety, and maintainability.
|
|
|
|
## Project Structure
|
|
- `src/`
|
|
- `routes/`
|
|
- `rest/` - REST API endpoints using Axum
|
|
- `routes/` - Individual route modules
|
|
- `ws/` - WebSocket handlers and related functionality
|
|
- `database/` - Database models, schema, and connection management
|
|
- `main.rs` - Application entry point and server setup
|
|
- `libs/`
|
|
- `sql_analyzer/` - SQL parsing and analysis with lineage tracking
|
|
- `src/` - Library source code
|
|
- `lib.rs` - Main entry point and API
|
|
- `types.rs` - Data structures for query analysis
|
|
- `errors.rs` - Custom error types
|
|
- `utils.rs` - SQL parsing and analysis utilities
|
|
|
|
## Implementation
|
|
When working with prds, you should always mark your progress off in them as you build.
|
|
|
|
## Database Connectivity
|
|
- The primary database connection is managed through `get_pg_pool()`, which returns a lazy static `PgPool`
|
|
- Always use this pool for database connections to ensure proper connection management
|
|
- Example usage:
|
|
```rust
|
|
let mut conn = get_pg_pool().get().await?;
|
|
```
|
|
|
|
## Code Style and Best Practices
|
|
|
|
### References and Memory Management
|
|
- Prefer references over owned values when possible
|
|
- Avoid unnecessary `.clone()` calls
|
|
- Use `&str` instead of `String` for function parameters when the string doesn't need to be owned
|
|
|
|
### Importing packages/crates
|
|
- Please make the dependency as short as possible in the actual logic by importing the crate/package.
|
|
|
|
### Database Operations
|
|
- Use Diesel for database migrations and query building
|
|
- Migrations are stored in the `migrations/` directory
|
|
|
|
### Concurrency Guidelines
|
|
- Prioritize concurrent operations, especially for:
|
|
- API requests
|
|
- File operations
|
|
- Optimize database connection usage:
|
|
- Batch operations where possible
|
|
- Build queries/parameters before executing database operations
|
|
- Use bulk inserts/updates instead of individual operations
|
|
```rust
|
|
// Preferred: Bulk operation
|
|
let items: Vec<_> = prepare_items();
|
|
diesel::insert_into(table)
|
|
.values(&items)
|
|
.execute(conn)?;
|
|
|
|
// Avoid: Individual operations in a loop
|
|
for item in items {
|
|
diesel::insert_into(table)
|
|
.values(&item)
|
|
.execute(conn)?;
|
|
}
|
|
```
|
|
|
|
### Error Handling
|
|
- Never use `.unwrap()` or `.expect()` in production code
|
|
- Always handle errors appropriately using:
|
|
- The `?` operator for error propagation
|
|
- `match` statements when specific error cases need different handling
|
|
- Use `anyhow` for error handling:
|
|
- Prefer `anyhow::Result<T>` as the return type for functions that can fail
|
|
- Use `anyhow::Error` for error types
|
|
- Use `anyhow!` macro for creating custom errors
|
|
```rust
|
|
use anyhow::{Result, anyhow};
|
|
|
|
// Example of proper error handling
|
|
pub async fn process_data(input: &str) -> Result<Data> {
|
|
// Use ? for error propagation
|
|
let parsed = parse_input(input)?;
|
|
|
|
// Use match when specific error cases need different handling
|
|
match validate_data(&parsed) {
|
|
Ok(valid_data) => Ok(valid_data),
|
|
Err(e) => Err(anyhow!("Data validation failed: {}", e))
|
|
}
|
|
}
|
|
|
|
// Avoid this:
|
|
// let data = parse_input(input).unwrap(); // ❌ Never use unwrap
|
|
```
|
|
|
|
### API Design
|
|
- REST endpoints should be in `routes/rest/routes/`
|
|
- WebSocket handlers should be in `routes/ws/`
|
|
- Use proper HTTP status codes
|
|
- Implement proper validation for incoming requests
|
|
|
|
### Testing
|
|
- Write unit tests for critical functionality
|
|
- Use integration tests for API endpoints
|
|
- Mock external dependencies when appropriate
|
|
|
|
## Common Patterns
|
|
|
|
### Database Queries
|
|
```rust
|
|
use diesel::prelude::*;
|
|
|
|
// Example of a typical database query
|
|
pub async fn get_item(id: i32) -> Result<Item> {
|
|
let pool = get_pg_pool();
|
|
let conn = pool.get().await?;
|
|
|
|
items::table
|
|
.filter(items::id.eq(id))
|
|
.first(&conn)
|
|
.map_err(Into::into)
|
|
}
|
|
```
|
|
|
|
### Concurrent Operations
|
|
```rust
|
|
use futures::future::try_join_all;
|
|
|
|
// Example of concurrent processing
|
|
let futures: Vec<_> = items
|
|
.into_iter()
|
|
.map(|item| process_item(item))
|
|
.collect();
|
|
let results = try_join_all(futures).await?;
|
|
```
|
|
|
|
Remember to always consider:
|
|
1. Connection pool limits when designing concurrent operations
|
|
2. Error handling for sequential related operations
|
|
3. Error propagation and cleanup
|
|
4. Memory usage and ownership
|
|
5. Please use comments to help document your code and make it more readable.
|
|
|
|
# Library Construction Guide
|
|
|
|
## Directory Structure
|
|
```
|
|
libs/
|
|
├── my_lib/
|
|
│ ├── Cargo.toml # Library-specific manifest
|
|
│ ├── src/
|
|
│ │ ├── lib.rs # Library root
|
|
│ │ ├── models/ # Data structures and types
|
|
│ │ ├── utils/ # Utility functions
|
|
│ │ └── errors.rs # Custom error types
|
|
│ └── tests/ # Integration tests
|
|
```
|
|
|
|
## Cargo.toml Template
|
|
```toml
|
|
[package]
|
|
name = "my_lib"
|
|
version = "0.1.0"
|
|
edition = "2021"
|
|
|
|
# Dependencies should be inherited from workspace
|
|
[dependencies]
|
|
# Use workspace dependencies
|
|
anyhow = { workspace = true }
|
|
chrono = { workspace = true }
|
|
serde = { workspace = true }
|
|
serde_json = { workspace = true }
|
|
tokio = { workspace = true }
|
|
tracing = { workspace = true }
|
|
uuid = { workspace = true }
|
|
diesel = { workspace = true }
|
|
diesel-async = { workspace = true }
|
|
# Add other workspace dependencies as needed
|
|
|
|
# Development dependencies
|
|
[dev-dependencies]
|
|
tokio-test = { workspace = true }
|
|
# Add other workspace dev dependencies as needed
|
|
|
|
# Feature flags
|
|
[features]
|
|
default = []
|
|
# Define library-specific features here
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### 1. Workspace Integration
|
|
- Use `{ workspace = true }` for common dependencies
|
|
- Never specify library-specific versions for dependencies that exist in the workspace
|
|
- All dependencies should be managed by the workspace
|
|
- Keep feature flags modular and specific to the library's needs
|
|
|
|
### 2. Library Structure
|
|
- Keep the library focused on a single responsibility
|
|
- Use clear module hierarchies
|
|
- Export public API through `lib.rs`
|
|
- Follow the workspace's common patterns
|
|
|
|
Example `lib.rs`:
|
|
```rust
|
|
//! My Library documentation
|
|
//!
|
|
//! This library provides...
|
|
|
|
// Re-export common workspace types if needed
|
|
pub use common_types::{Result, Error};
|
|
|
|
pub mod models;
|
|
pub mod utils;
|
|
mod errors;
|
|
|
|
// Re-exports
|
|
pub use errors::Error;
|
|
pub use models::{ImportantType, AnotherType};
|
|
```
|
|
|
|
### 3. Error Handling
|
|
- Use the workspace's common error types where appropriate
|
|
- Define library-specific errors only when needed
|
|
- Implement conversions to/from workspace error types
|
|
|
|
Example `errors.rs`:
|
|
```rust
|
|
use thiserror::Error;
|
|
use common_types::Error as WorkspaceError;
|
|
|
|
#[derive(Error, Debug)]
|
|
pub enum Error {
|
|
#[error("library specific error: {0}")]
|
|
LibrarySpecific(String),
|
|
|
|
#[error(transparent)]
|
|
Workspace(#[from] WorkspaceError),
|
|
}
|
|
```
|
|
|
|
### 4. Testing
|
|
- Follow workspace testing conventions
|
|
- Use shared test utilities from workspace when available
|
|
- Keep library-specific test helpers in the library
|
|
- Use workspace-defined test macros if available
|
|
|
|
### 5. Documentation
|
|
- Follow workspace documentation style
|
|
- Link to related workspace documentation
|
|
- Document workspace integration points
|
|
- Include examples showing workspace type usage
|
|
|
|
### 6. Integration Points
|
|
- Define clear boundaries with other workspace crates
|
|
- Use workspace traits and interfaces
|
|
- Share common utilities through workspace-level crates
|
|
- Consider cross-crate testing
|
|
|
|
### 7. Development Workflow
|
|
- Run workspace-level tests when making changes
|
|
- Update workspace documentation if needed
|
|
- Follow workspace versioning strategy
|
|
- Use workspace-level CI/CD pipelines
|
|
|
|
### 8. Dependencies
|
|
- All dependencies should be inherited from the workspace
|
|
- Never add library-specific dependency versions
|
|
- Keep dependencies minimal and focused
|
|
- The workspace will manage all dependency versions
|
|
|
|
# PRD (Product Requirements Document) Guidelines
|
|
|
|
## Overview
|
|
This document provides guidelines for creating and managing Product Requirements Documents (PRDs) in our codebase. All PRDs should follow the standardized template located at [template.md](mdc:prds/template.md)
|
|
|
|
## PRD Structure
|
|
|
|
### Location
|
|
All PRDs should be stored in the `/prds` directory with the following structure:
|
|
```
|
|
/prds
|
|
├── template.md # The master template for all PRDs
|
|
├── active/ # Active/In-progress PRDs
|
|
│ ├── feature_auth.md
|
|
│ └── api_deployment.md
|
|
├── completed/ # Completed PRDs that have been shipped
|
|
│ ├── feature_user_auth.md
|
|
│ └── api_deployment.md
|
|
└── archived/ # Archived/Deprecated PRDs
|
|
```
|
|
|
|
### Naming Convention
|
|
- Use snake_case for file names
|
|
- Include a prefix for the type of change:
|
|
- `feature_` for new features
|
|
- `enhancement_` for improvements
|
|
- `fix_` for bug fixes
|
|
- `refactor_` for code refactoring
|
|
- `api_` for API changes
|
|
|
|
## Using the Template
|
|
|
|
### Getting Started
|
|
1. Copy [template.md](mdc:prds/template.md) to create a new PRD
|
|
2. Place it in the `/prds/active` directory
|
|
3. Fill out each section following the template's comments and guidelines
|
|
|
|
### Key Sections to Focus On
|
|
The template [template.md](mdc:prds/template.md) provides comprehensive sections. Pay special attention to:
|
|
|
|
1. **Problem Statement**
|
|
- Must clearly articulate the current state
|
|
- Include measurable impact
|
|
- Reference any relevant metrics or data
|
|
|
|
2. **Technical Design**
|
|
- Include all affected components
|
|
- Document ALL file changes (new/modified/deleted)
|
|
- Provide actual code examples
|
|
- Include database migrations if needed
|
|
|
|
3. **Implementation Plan**
|
|
- Break down into deployable phases
|
|
- Include clear success criteria
|
|
- List dependencies between phases
|
|
- Provide testing strategy for each phase
|
|
|
|
4. **Testing Strategy**
|
|
- Unit test requirements
|
|
- Integration test scenarios
|
|
|
|
## Best Practices
|
|
|
|
### Documentation
|
|
1. Use clear, concise language
|
|
2. Include code examples where relevant
|
|
3. Document assumptions and dependencies
|
|
4. Keep diagrams up to date
|
|
5. Use Mermaid for diagrams when possible
|
|
|
|
### Lifecycle Management
|
|
1. Move PRDs between directories based on status:
|
|
- New PRDs → `/prds/active`
|
|
- Shipped PRDs → `/prds/completed`
|
|
- Deprecated PRDs → `/prds/archived`
|
|
|
|
2. Update status section regularly:
|
|
- ✅ Completed items
|
|
- ⏳ In Progress items
|
|
- 🔜 Upcoming items
|
|
- ❌ Known Issues
|
|
|
|
### Review Process
|
|
1. Technical review
|
|
- Architecture alignment
|
|
- Security considerations
|
|
- Performance implications
|
|
- Testing coverage
|
|
|
|
2. Product review
|
|
- Feature completeness
|
|
- User impact
|
|
- Business value
|
|
- Success metrics
|
|
|
|
## Common Pitfalls to Avoid
|
|
1. Incomplete technical specifications
|
|
2. Missing file change documentation
|
|
3. Unclear success criteria
|
|
4. Insufficient testing strategy
|
|
5. No rollback plan
|
|
6. Missing security considerations
|
|
7. Undefined monitoring metrics
|
|
|
|
## Example PRDs
|
|
Reference these example PRDs for guidance:
|
|
[template.md](mdc:prds/template.md)
|
|
|
|
## Checklist Before Submission
|
|
- [ ] All template sections completed
|
|
- [ ] Technical design is detailed and complete
|
|
- [ ] File changes are documented
|
|
- [ ] Implementation phases are clear (can be as many as you need.)
|
|
- [ ] Testing strategy is defined
|
|
- [ ] Security considerations addressed
|
|
- [ ] Dependencies and Files listed
|
|
- [ ] File References included
|
|
|
|
# REST API Formatting Rules
|
|
|
|
## Directory Structure
|
|
- All REST routes should be located under `src/routes/rest/routes/`
|
|
- Each resource should have its own directory (e.g., `api_keys`, `datasets`)
|
|
- Resource directories should contain individual files for each operation
|
|
- Each resource directory should have a `mod.rs` that exports and configures the routes
|
|
|
|
Example folder structure:
|
|
```
|
|
src/routes/rest/
|
|
├── routes/
|
|
│ ├── api_keys/
|
|
│ │ ├── mod.rs # Router configuration and exports
|
|
│ │ ├── list_api_keys.rs # GET / - Contains ApiKeyInfo type definition
|
|
│ │ ├── get_api_key.rs # GET /:id
|
|
│ │ ├── post_api_key.rs # POST /
|
|
│ │ └── delete_api_key.rs # DELETE /:id
|
|
│ │
|
|
│ ├── datasets/
|
|
│ │ ├── mod.rs
|
|
│ │ ├── list_datasets.rs # GET /
|
|
│ │ ├── get_dataset.rs # GET /:id
|
|
│ │ ├── post_dataset.rs # POST /
|
|
│ │ ├── update_dataset.rs # PUT /:id
|
|
│ │ ├── patch_dataset.rs # PATCH /:id
|
|
│ │ ├── delete_dataset.rs # DELETE /:id
|
|
│ │ └── deploy_dataset.rs # POST /:id/deploy (action endpoint)
|
|
│ │
|
|
│ └── users/
|
|
│ ├── mod.rs
|
|
│ ├── list_users.rs
|
|
│ ├── get_user.rs
|
|
│ ├── post_user.rs
|
|
│ ├── update_user.rs
|
|
│ └── api_keys/ # Sub-resource example
|
|
│ ├── mod.rs
|
|
│ ├── list_user_api_keys.rs
|
|
│ └── post_user_api_key.rs
|
|
```
|
|
|
|
Note: File names should be descriptive and match their HTTP operation (list_, get_, post_, update_, patch_, delete_). For action endpoints, use a descriptive verb (deploy_, publish_, etc.).
|
|
|
|
## Route Handler Pattern
|
|
- Each REST endpoint should follow a two-function pattern:
|
|
1. Main route handler (e.g., `get_api_key`) that:
|
|
- Handles HTTP-specific concerns (status codes, request/response types)
|
|
- Calls the business logic handler
|
|
- Wraps responses in `ApiResponse`
|
|
- Handles error conversion to HTTP responses
|
|
2. Business logic handler (e.g., `get_api_key_handler`) that:
|
|
- Contains pure business logic
|
|
- Returns `Result<T>` where T is your data type
|
|
- Can be reused across different routes (REST/WebSocket)
|
|
- Handles database operations and core functionality
|
|
|
|
## Type Definitions
|
|
- Response types should be defined in the corresponding list operation file (e.g., `ApiKeyInfo` in `list_api_keys.rs`)
|
|
- These types can be reused across different operations on the same resource
|
|
- Use strong typing with Rust structs for request/response bodies
|
|
|
|
## Router Configuration
|
|
- Each resource module should have a `mod.rs` that defines its router
|
|
- Use axum's `Router::new()` to define routes
|
|
- Group related routes with `.merge()`
|
|
- Apply middleware (like auth) at the router level where appropriate
|
|
- Follow RESTful patterns for endpoints:
|
|
- Collection endpoints (no ID):
|
|
- GET / - List resources
|
|
- POST / - Create resources (accepts single item or array)
|
|
- PUT / - Bulk update resources by criteria
|
|
- DELETE / - Bulk delete resources by criteria
|
|
- Single resource endpoints (with ID):
|
|
- GET /:id - Get single resource
|
|
- PUT /:id - Full update of resource (accepts single item or array of updates)
|
|
- PATCH /:id - Partial update of resource (accepts single item or array of patches)
|
|
- DELETE /:id - Delete resources (accepts single id or array of ids)
|
|
- Sub-resource endpoints:
|
|
- GET /:id/sub_resource - List sub-resources
|
|
- POST /:id/sub_resource - Create sub-resources (accepts single item or array)
|
|
- Action endpoints (for operations that don't fit CRUD):
|
|
- POST /:id/action_name - Perform specific action
|
|
- Example: POST /datasets/:id/deploy
|
|
- Query/Filter endpoints:
|
|
- GET /search - Complex search with query params
|
|
- GET /filter - Filtered list with specific criteria
|
|
|
|
Note: All mutation endpoints (POST, PUT, PATCH, DELETE) should accept both single items and arrays by default. The handler should handle both cases seamlessly. This eliminates the need for separate /bulk endpoints.
|
|
|
|
## Example Implementation
|
|
See @src/routes/rest/routes/api_keys/get_api_key.rs for a reference implementation that demonstrates:
|
|
- Separation of HTTP and business logic
|
|
- Error handling pattern
|
|
- Type usage and database operations
|
|
- Clean abstraction of business logic for potential reuse
|
|
|
|
## Error Handling
|
|
- Business logic handlers should return `Result<T>`
|
|
- REST handlers should convert errors to appropriate HTTP status codes
|
|
- Use `ApiResponse` enum for consistent response formatting
|
|
- Include appropriate error logging using `tracing`
|
|
|
|
## Middleware
|
|
- Most the time, every new route should be authenticated, unless specified differently by the user.
|
|
- Apply authentication and other middleware at the router level
|
|
- Use `route_layer` to apply middleware to groups of routes
|
|
- Keep middleware configuration in the resource's `mod.rs`
|
|
|
|
# Testing Rules and Best Practices
|
|
|
|
## General Testing Guidelines
|
|
- All tests must be async and use tokio test framework
|
|
- Tests should be well-documented with clear test case descriptions and expected outputs
|
|
- Each test should focus on testing a single piece of functionality
|
|
- Tests should be independent and not rely on the state of other tests
|
|
- Use meaningful test names that describe what is being tested
|
|
|
|
## Unit Tests
|
|
- Unit tests should be inline with the code they are testing using `#[cfg(test)]` modules
|
|
- Each public function should have corresponding unit tests
|
|
- Mock external dependencies using mockito for HTTP calls
|
|
- Use `mockito::Server::new_async()` instead of `mockito::Server::new()`
|
|
- Test both success and error cases
|
|
- Test edge cases and boundary conditions
|
|
|
|
## Integration Tests
|
|
- Integration tests should be placed in the `/tests` directory
|
|
- Organize integration tests to mirror the main codebase structure
|
|
- Each major feature/resource should have its own test file
|
|
- Test the interaction between multiple components
|
|
- Use real dependencies when possible, mock only what's necessary
|
|
- Include end-to-end workflow tests
|
|
- All integration tests must have services set and accessible via dotenv
|
|
|
|
### Integration Test Setup Requirements
|
|
- All integration tests must import and utilize the application's schema from [schema.rs](mdc:src/database/schema.rs)
|
|
- Database models from [models.rs](mdc:src/database/models.rs) should be used for test data setup and verification
|
|
- Environment setup must use `dotenv` for configuration:
|
|
```rust
|
|
use dotenv::dotenv;
|
|
|
|
#[tokio::test]
|
|
async fn setup_test_environment() {
|
|
dotenv().ok(); // Load environment variables
|
|
// Test environment setup
|
|
}
|
|
```
|
|
- Service configurations should be derived from environment variables:
|
|
```rust
|
|
// Example of service configuration using env vars
|
|
let database_url = std::env::var("DATABASE_URL")
|
|
.expect("DATABASE_URL must be set for integration tests");
|
|
let test_api_key = std::env::var("TEST_API_KEY")
|
|
.expect("TEST_API_KEY must be set for integration tests");
|
|
```
|
|
- Test database setup should include:
|
|
```rust
|
|
use crate::database::{schema, models};
|
|
|
|
async fn setup_test_db() -> PgPool {
|
|
let pool = PgPoolOptions::new()
|
|
.max_connections(5)
|
|
.connect(&std::env::var("TEST_DATABASE_URL")?)
|
|
.await?;
|
|
|
|
// Run migrations or setup test data
|
|
// Use schema and models for consistency
|
|
Ok(pool)
|
|
}
|
|
```
|
|
|
|
### Required Environment Variables
|
|
Create a `.env.test` file with necessary test configurations:
|
|
```env
|
|
TEST_DATABASE_URL=postgres://user:pass@localhost/test_db
|
|
TEST_API_KEY=test-key
|
|
TEST_ENV=test
|
|
# Add other required test environment variables
|
|
```
|
|
|
|
## Test Structure
|
|
```rust
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use mockito;
|
|
use tokio;
|
|
|
|
// Optional: Setup function for common test initialization
|
|
async fn setup() -> TestContext {
|
|
// Setup code here
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_name() {
|
|
// Test case description in comments
|
|
// Expected output in comments
|
|
|
|
// Arrange
|
|
// Setup test data and dependencies
|
|
|
|
// Act
|
|
// Execute the function being tested
|
|
|
|
// Assert
|
|
// Verify the results
|
|
}
|
|
}
|
|
```
|
|
|
|
## Mocking Guidelines
|
|
- Use mockito for HTTP service mocks
|
|
- Create mock responses that match real API responses
|
|
- Include both successful and error responses in mocks
|
|
- Clean up mocks after tests complete
|
|
|
|
## Error Testing
|
|
- Test error conditions and error handling
|
|
- Verify error messages and error types
|
|
- Test timeout scenarios
|
|
- Test connection failures
|
|
- Test invalid input handling
|
|
|
|
## Database Testing
|
|
- Use a separate test database for integration tests
|
|
- Clean up test data after tests complete
|
|
- Test database transactions and rollbacks
|
|
- Test database connection error handling
|
|
|
|
## Test Output
|
|
- Tests should provide clear error messages
|
|
- Use descriptive assert messages
|
|
- Print relevant debug information in test failures
|
|
- Log test execution progress for long-running tests
|
|
|
|
## CI/CD Considerations
|
|
- All tests must pass in CI environment
|
|
- Tests should be reproducible
|
|
- Tests should not have external dependencies that could fail CI
|
|
- Test execution time should be reasonable
|
|
|
|
## Example Test
|
|
```rust
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use mockito;
|
|
use tokio;
|
|
|
|
#[tokio::test]
|
|
async fn test_api_call_success() {
|
|
// Test case: Successful API call returns expected response
|
|
// Expected: Response contains user data with status 200
|
|
|
|
let mut server = mockito::Server::new_async().await;
|
|
|
|
let mock = server
|
|
.mock("GET", "/api/user")
|
|
.match_header("authorization", "Bearer test-token")
|
|
.with_status(200)
|
|
.with_body(r#"{"id": "123", "name": "Test User"}"#)
|
|
.create();
|
|
|
|
let client = ApiClient::new(server.url());
|
|
let response = client.get_user().await.unwrap();
|
|
|
|
assert_eq!(response.id, "123");
|
|
assert_eq!(response.name, "Test User");
|
|
|
|
mock.assert();
|
|
}
|
|
}
|
|
```
|
|
|
|
## Example Integration Test
|
|
```rust
|
|
use crate::database::{models, schema};
|
|
use dotenv::dotenv;
|
|
|
|
#[tokio::test]
|
|
async fn test_user_creation_flow() {
|
|
// Load test environment
|
|
dotenv().ok();
|
|
|
|
// Setup test database connection
|
|
let pool = setup_test_db().await.expect("Failed to setup test database");
|
|
|
|
// Create test user using models
|
|
let test_user = models::User {
|
|
id: Uuid::new_v4(),
|
|
email: "test@example.com".to_string(),
|
|
name: Some("Test User".to_string()),
|
|
config: serde_json::Value::Null,
|
|
created_at: Utc::now(),
|
|
updated_at: Utc::now(),
|
|
attributes: serde_json::Value::Null,
|
|
};
|
|
|
|
// Use schema for database operations
|
|
diesel::insert_into(schema::users::table)
|
|
.values(&test_user)
|
|
.execute(&mut pool.get().await?)
|
|
.expect("Failed to insert test user");
|
|
|
|
// Test application logic
|
|
let response = create_test_client()
|
|
.get("/api/users")
|
|
.send()
|
|
.await?;
|
|
|
|
assert_eq!(response.status(), 200);
|
|
// Additional assertions...
|
|
}
|
|
```
|
|
|
|
## Common Test Utilities
|
|
- All shared test utilities should be placed in `tests/common/mod.rs`
|
|
- Common database setup and teardown functions should be in `tests/common/db.rs`
|
|
- Environment setup utilities should be in `tests/common/env.rs`
|
|
- Shared test fixtures should be in `tests/common/fixtures/`
|
|
|
|
### Common Test Module Structure
|
|
```
|
|
tests/
|
|
├── common/
|
|
│ ├── mod.rs # Main module file that re-exports all common utilities
|
|
│ ├── db.rs # Database setup/teardown utilities
|
|
│ ├── env.rs # Environment configuration utilities
|
|
│ ├── fixtures/ # Test data fixtures
|
|
│ │ ├── mod.rs # Exports all fixtures
|
|
│ │ ├── users.rs # User-related test data
|
|
│ │ └── threads.rs # Thread-related test data
|
|
│ └── helpers.rs # General test helper functions
|
|
└── integration/ # Integration test files
|
|
```
|
|
|
|
### Common Database Setup
|
|
```rust
|
|
// tests/common/db.rs
|
|
use diesel::PgConnection;
|
|
use diesel::r2d2::{ConnectionManager, Pool};
|
|
use crate::database::{models, schema};
|
|
use dotenv::dotenv;
|
|
|
|
pub struct TestDb {
|
|
pub pool: Pool<ConnectionManager<PgConnection>>,
|
|
}
|
|
|
|
impl TestDb {
|
|
pub async fn new() -> anyhow::Result<Self> {
|
|
dotenv().ok();
|
|
|
|
let database_url = std::env::var("TEST_DATABASE_URL")
|
|
.expect("TEST_DATABASE_URL must be set");
|
|
|
|
let manager = ConnectionManager::<PgConnection>::new(database_url);
|
|
let pool = Pool::builder()
|
|
.max_size(5)
|
|
.build(manager)?;
|
|
|
|
Ok(Self { pool })
|
|
}
|
|
|
|
pub async fn setup_test_data(&self) -> anyhow::Result<()> {
|
|
// Add common test data setup here
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn cleanup(&self) -> anyhow::Result<()> {
|
|
// Cleanup test data
|
|
Ok(())
|
|
}
|
|
}
|
|
```
|
|
|
|
### Common Environment Setup
|
|
```rust
|
|
// tests/common/env.rs
|
|
use std::sync::Once;
|
|
use dotenv::dotenv;
|
|
|
|
static ENV_SETUP: Once = Once::new();
|
|
|
|
pub fn setup_test_env() {
|
|
ENV_SETUP.call_once(|| {
|
|
dotenv().ok();
|
|
// Set any default environment variables for tests
|
|
std::env::set_var("TEST_ENV", "test");
|
|
});
|
|
}
|
|
```
|
|
|
|
### Example Test Fixtures
|
|
```rust
|
|
// tests/common/fixtures/users.rs
|
|
use crate::database::models::User;
|
|
use chrono::Utc;
|
|
use uuid::Uuid;
|
|
|
|
pub fn create_test_user() -> User {
|
|
User {
|
|
id: Uuid::new_v4(),
|
|
email: "test@example.com".to_string(),
|
|
name: Some("Test User".to_string()),
|
|
config: serde_json::Value::Null,
|
|
created_at: Utc::now(),
|
|
updated_at: Utc::now(),
|
|
attributes: serde_json::Value::Null,
|
|
}
|
|
}
|
|
```
|
|
|
|
### Using Common Test Utilities
|
|
```rust
|
|
// Example integration test using common utilities
|
|
use crate::tests::common::{db::TestDb, env::setup_test_env, fixtures};
|
|
|
|
#[tokio::test]
|
|
async fn test_user_creation() {
|
|
// Setup test environment
|
|
setup_test_env();
|
|
|
|
// Initialize test database
|
|
let test_db = TestDb::new().await.expect("Failed to setup test database");
|
|
|
|
// Get test user fixture
|
|
let test_user = fixtures::users::create_test_user();
|
|
|
|
// Run test
|
|
let result = create_user(&test_db.pool, &test_user).await?;
|
|
|
|
// Cleanup
|
|
test_db.cleanup().await?;
|
|
|
|
assert!(result.is_ok());
|
|
}
|
|
```
|
|
|
|
# Tools Documentation and Guidelines
|
|
|
|
## Overview
|
|
This document outlines the architecture, patterns, and best practices for building tools in our system. Tools are modular, reusable components that provide specific functionality to our AI agents and application.
|
|
|
|
## Core Architecture
|
|
|
|
### ToolExecutor Trait
|
|
The foundation of our tools system is the `ToolExecutor` trait. Any struct that wants to be used as a tool must implement this trait:
|
|
|
|
```rust
|
|
#[async_trait]
|
|
pub trait ToolExecutor: Send + Sync {
|
|
type Output: Serialize + Send;
|
|
async fn execute(&self, tool_call: &ToolCall) -> Result<Self::Output>;
|
|
fn get_schema(&self) -> serde_json::Value;
|
|
fn get_name(&self) -> String;
|
|
}
|
|
```
|
|
|
|
Key components:
|
|
- `Output`: The return type of your tool (must be serializable)
|
|
- `execute()`: The main function that implements your tool's logic
|
|
- `get_schema()`: Returns the JSON schema describing the tool's interface
|
|
- `get_name()`: Returns the tool's unique identifier
|
|
|
|
## Tool Categories
|
|
|
|
### 1. File Tools
|
|
Our file tools provide a robust example of well-structured tool implementation. They handle:
|
|
- File creation and modification
|
|
- File searching and cataloging
|
|
- File type-specific operations
|
|
- User interaction with files
|
|
|
|
Key patterns from file tools:
|
|
- Modular organization by functionality
|
|
- Clear separation of concerns
|
|
- Type-safe file operations
|
|
- Consistent error handling
|
|
|
|
### 2. Interaction Tools
|
|
Tools that manage user and system interactions.
|
|
|
|
## Best Practices
|
|
|
|
### 1. Tool Structure
|
|
- Create a new module for each tool category
|
|
- Implement the `ToolExecutor` trait
|
|
- Use meaningful types for `Output`
|
|
- Provide comprehensive error handling
|
|
|
|
### 2. Schema Design
|
|
- Document all parameters clearly
|
|
- Use descriptive names for properties
|
|
- Include example values where helpful
|
|
- Validate input parameters
|
|
|
|
### 3. Error Handling
|
|
- Use `anyhow::Result` for flexible error handling
|
|
- Provide meaningful error messages
|
|
- Handle edge cases gracefully
|
|
- Implement proper error propagation
|
|
|
|
### 4. Testing
|
|
- Write unit tests for each tool
|
|
- Test edge cases and error conditions
|
|
- Mock external dependencies
|
|
- Ensure thread safety for async operations
|
|
|
|
## Creating New Tools
|
|
|
|
### Step 1: Define Your Tool
|
|
```rust
|
|
pub struct MyNewTool {
|
|
// Tool-specific fields
|
|
}
|
|
|
|
#[async_trait]
|
|
impl ToolExecutor for MyNewTool {
|
|
type Output = YourOutputType;
|
|
|
|
async fn execute(&self, tool_call: &ToolCall) -> Result<Self::Output> {
|
|
// Implementation
|
|
}
|
|
|
|
fn get_schema(&self) -> Value {
|
|
// Schema definition
|
|
}
|
|
|
|
fn get_name(&self) -> String {
|
|
"my_new_tool".to_string()
|
|
}
|
|
}
|
|
```
|
|
|
|
### Step 2: Schema Definition
|
|
```json
|
|
{
|
|
"name": "my_new_tool",
|
|
"description": "Clear description of what the tool does",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
// Tool parameters
|
|
},
|
|
"required": ["param1", "param2"]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Step 3: Integration
|
|
1. Add your tool to the appropriate module
|
|
2. Register it in the tool registry
|
|
3. Add necessary tests
|
|
4. Document usage examples
|
|
|
|
## Common Patterns
|
|
|
|
### Value Conversion
|
|
Use `IntoValueTool` trait when you need to convert tool output to generic JSON:
|
|
```rust
|
|
my_tool.into_value_tool()
|
|
```
|
|
|
|
### File Operations
|
|
For tools that modify files:
|
|
- Implement `FileModificationTool` trait
|
|
- Use `add_line_numbers` for better output formatting
|
|
- Handle file permissions appropriately
|
|
|
|
## Security Considerations
|
|
1. Validate all input parameters
|
|
2. Check file permissions before operations
|
|
3. Sanitize file paths
|
|
4. Handle sensitive data appropriately
|
|
|
|
## Examples
|
|
|
|
### File Tool Example
|
|
```rust
|
|
pub struct ReadFileTool {
|
|
base_path: PathBuf,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl ToolExecutor for ReadFileTool {
|
|
type Output = String;
|
|
|
|
async fn execute(&self, tool_call: &ToolCall) -> Result<Self::Output> {
|
|
// Implementation
|
|
}
|
|
}
|
|
```
|
|
|
|
### Interaction Tool Example
|
|
```rust
|
|
pub struct UserPromptTool;
|
|
|
|
#[async_trait]
|
|
impl ToolExecutor for UserPromptTool {
|
|
type Output = UserResponse;
|
|
|
|
async fn execute(&self, tool_call: &ToolCall) -> Result<Self::Output> {
|
|
// Implementation
|
|
}
|
|
}
|
|
```
|
|
|
|
## Troubleshooting
|
|
1. Check tool registration
|
|
2. Verify schema correctness
|
|
3. Ensure proper error handling
|
|
4. Validate async operations
|
|
|
|
## Future Considerations
|
|
1. Tool versioning
|
|
2. Performance optimization
|
|
3. Enhanced type safety
|
|
4. Extended testing frameworks
|
|
|
|
# WebSocket API Formatting Rules
|
|
|
|
- Commonly used types and functions [ws_utils.rs](mdc:src/routes/ws/ws_utils.rs), [ws_router.rs](mdc:src/routes/ws/ws_router.rs)
|
|
|
|
## Directory Structure
|
|
- All WebSocket routes should be located under `src/routes/ws/`
|
|
- Each resource should have its own directory (e.g., `sql`, `datasets`, `threads_and_messages`)
|
|
- Resource directories should contain individual files for each operation
|
|
- Each resource directory should have a `mod.rs` that exports the routes and types
|
|
|
|
Example folder structure:
|
|
```
|
|
src/routes/ws/
|
|
├── sql/
|
|
│ ├── mod.rs
|
|
│ ├── sql_router.rs # Contains SqlRoute enum and router function
|
|
│ └── run_sql.rs # Contains handler implementation
|
|
├── datasets/
|
|
│ ├── mod.rs
|
|
│ ├── datasets_router.rs # Contains DatasetRoute enum and router function
|
|
│ └── list_datasets.rs # Contains handler implementation
|
|
└── threads_and_messages/
|
|
├── mod.rs
|
|
├── threads_router.rs
|
|
└── post_thread/
|
|
├── mod.rs
|
|
├── post_thread.rs
|
|
└── agent_thread.rs
|
|
```
|
|
|
|
## WebSocket Message Handling with Redis
|
|
- WebSocket messages are handled through Redis streams for reliable message delivery
|
|
- Key utilities in [ws_utils.rs](mdc:src/routes/ws/ws_utils.rs) handle message sending, subscriptions, and error handling
|
|
- Messages are compressed using GZip before being sent through Redis
|
|
|
|
### Message Sending Pattern
|
|
1. Messages are sent using `send_ws_message`:
|
|
- Serializes the `WsResponseMessage` to JSON
|
|
- Compresses the message using GZip
|
|
- Adds the message to a Redis stream with a maxlen of 50
|
|
```rust
|
|
pub async fn send_ws_message(subscription: &String, message: &WsResponseMessage) -> Result<()> {
|
|
// Serialize and compress message
|
|
let message_string = serde_json::to_string(&message)?;
|
|
let mut compressed = Vec::new();
|
|
let mut encoder = GzipEncoder::new(&mut compressed);
|
|
encoder.write_all(message_string.as_bytes()).await?;
|
|
|
|
// Send to Redis stream
|
|
redis_conn.xadd_maxlen(
|
|
&subscription,
|
|
StreamMaxlen::Approx(50),
|
|
"*",
|
|
&[("data", &compressed)]
|
|
).await?;
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Subscription Management
|
|
1. Subscribe to streams using `subscribe_to_stream`:
|
|
- Creates a new Redis stream group if it doesn't exist
|
|
- Adds the subscription to the tracked subscriptions
|
|
- Notifies the user's stream of the new subscription
|
|
```rust
|
|
subscribe_to_stream(
|
|
subscriptions: &SubscriptionRwLock,
|
|
new_subscription: &String,
|
|
user_group: &String,
|
|
user_id: &Uuid,
|
|
)
|
|
```
|
|
|
|
2. Unsubscribe from streams using `unsubscribe_from_stream`:
|
|
- Removes subscription from tracked subscriptions
|
|
- Destroys the Redis stream group
|
|
- Cleans up Redis keys if no groups remain
|
|
- Handles draft subscriptions cleanup
|
|
```rust
|
|
unsubscribe_from_stream(
|
|
subscriptions: &Arc<SubscriptionRwLock>,
|
|
subscription: &String,
|
|
user_group: &String,
|
|
user_id: &Uuid,
|
|
)
|
|
```
|
|
|
|
### Key-Value Operations
|
|
- Temporary data can be stored using Redis key-value operations:
|
|
- `set_key_value`: Sets a key with 1-hour expiration
|
|
- `get_key_value`: Retrieves a stored value
|
|
- `delete_key_value`: Removes a stored value
|
|
|
|
### Error Message Pattern
|
|
- Use `send_error_message` for consistent error handling:
|
|
```rust
|
|
send_error_message(
|
|
subscription: &String,
|
|
route: WsRoutes,
|
|
event: WsEvent,
|
|
code: WsErrorCode,
|
|
message: String,
|
|
user: &AuthenticatedUser,
|
|
)
|
|
```
|
|
|
|
## Route Handler Pattern
|
|
Each WebSocket endpoint should follow a two-function pattern:
|
|
|
|
1. Main route handler (e.g., `run_sql`) that:
|
|
- Takes a request payload and user information
|
|
- Calls the business logic handler
|
|
- Handles sending WebSocket messages using `send_ws_message`
|
|
- Returns `Result<()>` since WebSocket responses are sent asynchronously
|
|
|
|
2. Business logic handler (e.g., `run_sql_handler`) that:
|
|
- Contains pure business logic
|
|
- Returns `Result<T>` where T is your data type
|
|
- Can be reused across different routes (REST/WebSocket)
|
|
- Handles database operations and core functionality
|
|
|
|
## Route Enums and Router Configuration
|
|
- Each resource should have a router file (e.g., `sql_router.rs`) that defines:
|
|
1. A Route enum for path matching (e.g., `SqlRoute`)
|
|
2. An Event enum for event types (e.g., `SqlEvent`)
|
|
3. A router function that matches routes to handlers
|
|
|
|
Example Route Enum:
|
|
```rust
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
pub enum SqlRoute {
|
|
#[serde(rename = "/sql/run")]
|
|
Run,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum SqlEvent {
|
|
RunSql,
|
|
}
|
|
```
|
|
|
|
Example Router Function:
|
|
```rust
|
|
pub async fn sql_router(route: SqlRoute, data: Value, user: &AuthenticatedUser) -> Result<()> {
|
|
match route {
|
|
SqlRoute::Run => {
|
|
let req = serde_json::from_value(data)?;
|
|
run_sql(user, req).await?;
|
|
}
|
|
};
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
## WebSocket Response Pattern
|
|
- All WebSocket responses use the `WsResponseMessage` type
|
|
- Messages are sent using the `send_ws_message` function
|
|
- Responses include:
|
|
- The route that triggered the response
|
|
- The event type
|
|
- The response payload
|
|
- Optional metadata
|
|
- User information
|
|
- Send method (e.g., SenderOnly, Broadcast)
|
|
|
|
Example Response:
|
|
```rust
|
|
let response = WsResponseMessage::new(
|
|
WsRoutes::Sql(SqlRoute::Run),
|
|
WsEvent::Sql(SqlEvent::RunSql),
|
|
response_data,
|
|
None,
|
|
user,
|
|
WsSendMethod::SenderOnly,
|
|
);
|
|
|
|
send_ws_message(&user.id.to_string(), &response).await?;
|
|
```
|
|
|
|
## Error Handling
|
|
- Business logic handlers should return `Result<T>`
|
|
- WebSocket handlers should convert errors to appropriate error messages
|
|
- Use `send_error_message` for consistent error formatting
|
|
- Include appropriate error logging using `tracing`
|
|
|
|
Example Error Handling:
|
|
```rust
|
|
if let Err(e) = result {
|
|
tracing::error!("Error: {}", e);
|
|
send_error_message(
|
|
&user.id.to_string(),
|
|
WsRoutes::Sql(SqlRoute::Run),
|
|
WsEvent::Sql(SqlEvent::RunSql),
|
|
WsErrorCode::InternalServerError,
|
|
e.to_string(),
|
|
user,
|
|
).await?;
|
|
return Err(anyhow!(e));
|
|
}
|
|
```
|
|
|
|
## Main WebSocket Router
|
|
- The main router (`ws_router.rs`) contains the `WsRoutes` enum
|
|
- Each resource route enum is a variant in `WsRoutes`
|
|
- The router parses the incoming path and routes to the appropriate handler
|
|
- Follows pattern:
|
|
1. Parse route from path string
|
|
2. Match on route type
|
|
3. Call appropriate resource router
|
|
|
|
Example:
|
|
```rust
|
|
pub enum WsRoutes {
|
|
Sql(SqlRoute),
|
|
Datasets(DatasetRoute),
|
|
Threads(ThreadRoute),
|
|
// ... other routes
|
|
}
|
|
|
|
pub async fn ws_router(
|
|
route: String,
|
|
payload: Value,
|
|
subscriptions: &Arc<SubscriptionRwLock>,
|
|
user_group: &String,
|
|
user: &AuthenticatedUser,
|
|
) -> Result<()> {
|
|
let parsed_route = WsRoutes::from_str(&route)?;
|
|
match parsed_route {
|
|
WsRoutes::Sql(sql_route) => {
|
|
sql_router(sql_route, payload, user).await
|
|
},
|
|
// ... handle other routes
|
|
}
|
|
}
|
|
```
|
|
|
|
## Example Implementation
|
|
See @src/routes/ws/sql/run_sql.rs for a reference implementation that demonstrates:
|
|
- Separation of WebSocket and business logic
|
|
- Error handling pattern
|
|
- Type usage and database operations
|
|
- WebSocket message sending pattern
|
|
- Clean abstraction of business logic for potential reuse |