Introduction
Welcome to the Biyard Rust Development Guide. This documentation serves as a comprehensive resource for developers to effectively build, structure, and maintain Rust-based applications within the Biyard ecosystem.
The guide covers project structure, coding standards, development patterns, and practical implementations of web applications, backend services, and blockchain integration using Rust.
Purpose
The main objective of this documentation is to:
- Standardize Rust project setups and structures for consistency across various Biyard projects.
- Provide clear guidelines and best practices for creating maintainable, scalable, and robust Rust applications.
- Facilitate seamless integration between frontend applications, backend services, and blockchain components (ICP).
Audience
This guide is intended for:
- New developers onboarding into Rust projects at Biyard.
- Existing developers looking for best practices, standard patterns, and implementation references.
- Technical leads and architects defining guidelines and ensuring consistency across Rust projects.
Document Overview
This guide is structured into several key sections:
- Introduction: Overview of the project and general guidelines for Rust development.
- Common Package: Standard components shared across projects, including model definitions, DTOs, validation, and repositories.
- Frontend: Detailed instructions and structures for developing web and mobile frontend applications.
- Backend: Guidelines for developing backend services, including API design, implementation, and testing.
- ICP: Instructions for developing and deploying smart contracts (canisters) on the Internet Computer blockchain.
- CI/CD: Best practices and configurations for continuous integration and deployment, including automated testing strategies.
Please navigate through the documentation using the sidebar or links provided in each section.
Project Structure
This section describes the standard package structure for Biyard Rust projects, providing clarity on the organization and purpose of each directory and file.
Basic Project Structure
The basic structure of a Biyard Rust project is organized as follows:
.
├── Cargo.toml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CODEOWNERS
├── CONTRIBUTING.md
├── Makefile
├── packages
│ ├── common
│ ├── api
│ └── www
├── README.md
Explanation of Directories
Root Directory
- Cargo.toml: Defines Rust project dependencies and workspace configuration.
- CHANGELOG.md: Documents changes and updates to the project.
- CODE_OF_CONDUCT.md, CODEOWNERS, CONTRIBUTING.md: Guidelines and instructions for contributing and managing community interactions.
- Makefile: Contains build tasks and command shortcuts to streamline development processes.
packages
- common: Shared utilities, models, data transfer objects (DTOs), validation logic, and common infrastructure used across various subdomains.
- api: Contains backend API implementations and related server-side logic specific to the API subdomain.
- www: Frontend web interface, UI components, and related assets specific to the web subdomain.
This standardized structure ensures clear separation of concerns, maintainability, and ease of collaboration across Biyard Rust projects.
- For example, if the project is desired for
example.com
,packages/api
should be implemented forapi.example.com
. - Similarly,
packages/www
indicates implementation ofwww.example.com
andexample.com
.
Cargo.toml
The root Cargo.toml
file defines the Rust workspace configuration and global dependencies used throughout the project. Below is a typical example:
[workspace]
members = ["packages/*"]
resolver = "2"
exclude = ["deps"]
[workspace.package]
authors = ["Biyard"]
description = "Ratel"
edition = "2024"
repository = "https://github.com/biyard/service-repo"
license = "MIT"
description = "Project descrption"
[workspace.dependencies]
bdk = { path = "deps/rust-sdk/packages/bdk" }
sqlx = { version = "0.8.3", features = [
"sqlite",
"postgres",
"runtime-tokio",
"time",
"bigdecimal",
] }
Key Dependencies
- bdk: A crucial crate within the Biyard ecosystem, which encapsulates essential tools and libraries such as
dioxus
,axum
, and other common utilities. Leveraging bdk promotes consistency, simplifies maintenance, and accelerates development by providing a pre-configured stack of widely-used Rust crates tailored specifically for Biyard projects. - sqlx: Provides robust, type-safe interactions with databases, configured to support both SQLite and PostgreSQL with asynchronous runtime provided by Tokio.
This standardized structure ensures clear separation of concerns, maintainability, and ease of collaboration across Biyard Rust projects.
Package Structure
This document outlines the standardized directory structure for the Common Package within Biyard Rust projects. It provides shared components such as Data Transfer Objects (DTOs), database models, utilities, and error definitions used consistently throughout the workspace.
Directory Structure
The common
package structure is as follows:
common
├── lib.rs
├── error.rs
├── dto
│ ├── mod.rs
│ └── v1
│ ├── users.rs
│ └── users
│ └── codes.rs
├── tables
│ ├── mod.rs
│ ├── users
│ │ ├── mod.rs
│ │ └── user.rs
│ └── products
│ ├── mod.rs
│ └── product.rs
└── utils
Explanation of Structure
Root Files
-
lib.rs
Serves as the entry point, defining module exports and public interfaces for the common crate. -
error.rs
Defines common error types and shared error-handling utilities.
dto
(Data Transfer Objects)
- DTO definitions are organized according to their corresponding API paths, reflecting API endpoints clearly.
- The directory hierarchy directly matches the API structure to maintain consistency and clarity.
Example:
If the API endpoint is defined as:
GET /v1/users/codes
Then the DTO path should be:
dto/v1/users/codes.rs
tables
(Database Models)
- Structured by database tables, each directory containing a
mod.rs
to export models clearly. - Individual database models are organized by table name.
Example structure:
tables/users/mod.rs // Module exports for user models
tables/users/user.rs // Definition of the User model
utils
(Utility Functions)
- Contains common helper functions such as data validation, parsing, date/time utilities, and general-purpose tools.
Example structure:
utils/time.rs // Utilities for time-related functions
utils/validation.rs // Common validation functions
lib.rs
Structure
The lib.rs
file serves as the entry point and central module definition for the Common Package within Biyard Rust projects. It organizes module visibility, re-exports commonly used types, and simplifies access to shared functionality throughout the workspace.
Example Template
#![allow(unused)] fn main() { // packages/common/lib.rs pub mod dto; pub mod tables; pub mod utils; pub mod error; pub mod prelude { pub use crate::dto::*; pub use crate::tables::*; pub use crate::utils::*; pub use crate::error::*; } pub type Result<T> = std::result::Result<T, crate::error::Error>; }
Explanation of Components
Module Exports
The following modules are declared publicly to enable their usage across the workspace:
dto
: Contains Data Transfer Objects structured according to API paths.tables
: Database models organized by table names.utils
: Shared utility functions used throughout the workspace.error
: Centralized error definitions and handling mechanisms.
prelude
Module
The prelude
module simplifies imports for commonly used components from the common package. By re-exporting essential types and utilities, developers can streamline imports significantly.
Usage Example:
Instead of multiple imports:
#![allow(unused)] fn main() { use common::dto::*; use common::tables::*; use common::utils::*; use common::error::*; }
Simply use:
#![allow(unused)] fn main() { use common::prelude::*; }
This improves readability and reduces boilerplate in application code.
Type Alias (Result<T>
)
The custom type alias:
#![allow(unused)] fn main() { pub type Result<T> = std::result::Result<T, crate::error::Error>; }
provides a convenient and consistent way to handle results throughout the project. It ensures uniform error handling and clearly communicates potential errors from functions.
Example Usage:
#![allow(unused)] fn main() { use common::prelude::*; pub async fn fetch_user(user_id: i32) -> Result<User> { // implementation logic... } }
This approach streamlines error handling by clearly defining the expected error type for all operations within the workspace.
Error Handling
This document describes the standardized approach to defining and managing errors within Biyard Rust projects using the custom Error
enum. It provides structured error definitions, translation support for internationalization (i18n), and integration with common external error types.
Error Enum Definition
Errors are centralized in the common/error.rs
file, defined as an enum with descriptive variants, each supporting multilingual translation using the Translate
macro.
Example Definition
#![allow(unused)] fn main() { // packages/common/error.rs use bdk::prelude::*; #[derive(Debug, serde::Serialize, PartialEq, Eq, serde::Deserialize, Translate)] #[cfg_attr(feature = "server", derive(schemars::JsonSchema, aide::OperationIo))] pub enum Error { #[translate( ko = "회원가입에 실패했습니다. 다시 시도해주세요.", en = "Sign-up failed. Please try again." )] SignupFailed(String), #[translate( ko = "API 호출에 실패하였습니다. 네트워크 연결상태를 확인해주세요.", en = "Failed to call API. Please check network status." )] ApiCallError(String), #[translate(ko = "입력값이 잘못되었습니다.", en = "Invalid input value.")] ValidationError(String), #[translate( ko = "데이터베이스 쿼리에 실패하였습니다. 입력값을 확인해주세요.", en = "Failed to execute database query. Please check your inputs." )] DatabaseError(String), } }
Explanation of Variants
Each variant clearly represents a specific error scenario with a descriptive message:
SignupFailed
: Indicates failure during the user sign-up process.ApiCallError
: Represents errors arising from failed external API calls.ValidationError
: Captures errors resulting from invalid user input or validation failures.DatabaseError
: Indicates failure during database operations.
All variants carry a descriptive error message (String
) for detailed context.
Translation Support
Each error variant uses the custom #[translate]
macro to provide clear, user-friendly multilingual error messages. This simplifies internationalization (i18n) and ensures users receive clear, language-specific feedback.
Example:
#![allow(unused)] fn main() { #[translate( ko = "입력값이 잘못되었습니다.", en = "Invalid input value." )] ValidationError(String) }
Conversions from External Errors
The custom error enum provides automatic conversions from common external crate errors (reqwest
, validator
, sqlx
), simplifying error handling throughout the application:
#![allow(unused)] fn main() { impl From<reqwest::Error> for Error { fn from(e: reqwest::Error) -> Self { Error::ApiCallError(e.to_string()) } } impl From<validator::ValidationErrors> for Error { fn from(e: validator::ValidationErrors) -> Self { Error::ValidationError(e.to_string()) } } #[cfg(feature = "server")] impl From<sqlx::Error> for Error { fn from(e: sqlx::Error) -> Self { Error::DatabaseError(e.to_string()) } } }
These conversions streamline error propagation and allow consistent handling of external library errors.
Server Integration (Axum Response)
The error enum integrates seamlessly with the Axum web framework (when the server
feature is enabled) to provide standardized HTTP responses:
#![allow(unused)] fn main() { #[cfg(feature = "server")] impl IntoResponse for ApiError { fn into_response(self) -> Response { let status_code = match &self { _ => StatusCode::BAD_REQUEST, }; let body = Json(self); (status_code, body).into_response() } } }
This ensures consistent HTTP response formatting for errors, simplifying frontend error handling.
Recommended Usage Pattern
When defining API endpoints or business logic functions, leverage the custom Result
alias to clearly communicate potential errors:
#![allow(unused)] fn main() { use common::prelude::*; pub async fn create_user(user: User) -> Result<User> { if user.username.is_empty() { return Err(Error::ValidationError("Username cannot be empty.".into())); } // Database operation that could fail, automatically converting errors let saved_user = save_to_db(user).await?; Ok(saved_user) } }
Models
This section provides guidelines for defining and managing data models within Biyard Rust projects. Models represent structured data closely aligned with database entities and API responses, ensuring clear, maintainable, and efficient data handling across different layers of the application.
The goals of this section include:
- Standardizing the definition of database models.
- Providing consistent patterns for API request and response data structures.
- Simplifying data validation, serialization, and integration with database queries.
To achieve these goals, we extensively use the api_model
macro, which streamlines model definitions by automatically generating serialization, validation, and database integration logic.
Detailed explanations and practical examples of using the api_model
macro are provided in the subsections.
Define a Structure
This document describes how to define structured data models within Biyard Rust projects using the custom api_model
macro, streamlining serialization, validation, API interactions, and database integrations.
🛠️ Using the api_model
Macro
The api_model
macro supports two types of attributes:
- Structure Attributes: Applied to the entire struct, specifying API endpoints, supported actions, database tables, and response behaviors.
- Field Attributes: Applied individually to fields, specifying primary keys, relationships, and serialization rules.
This document specifically explains structure attributes.
📌 Structure Attributes
Structure attributes configure API endpoints, database associations, and model-specific actions:
api_model
automatically attaches derives ofDebug
,Clone
,serde::Deserialize
,serde::Serialize
,Default
,PartialEq
by default.- It also add
schemars::JsonSchema
andaide::OperationIo
forserver
feature.
Syntax Example:
#![allow(unused)] fn main() { #[api_model(base = "/v1/users", table = "users")] pub struct User { #[api_model(primary_key)] pub id: i64, pub username: String, } }
📖 Attribute Definitions
Attribute | Description | Example |
---|---|---|
base | API path prefix for model-related endpoints. | base = "/v1/users" |
table | Specifies the database table associated with the model. | table = "users" |
queryable | Enables query functionality with optional sorting/filtering parameters. | queryable = [(sort = Sorter)] |
action | Defines actions (e.g., POST requests) with optional custom parameters. | action = [signup(code = String, param = u64)] |
read_action | Defines custom read-only actions without parameters. | read_action = read_data |
action_by_id | Defines actions targeted by entity ID (e.g., update, delete). | action_by_id = [get_data, update_data] |
response | Specifies custom response types for defined actions. | response = [signup(UserResponse)] |
📖 Field Attributes
Field attributes specify behaviors at the individual field level:
Attribute | Description | Example |
---|---|---|
summary | Adds the field to UserSummary . | #[api_model(summary)] |
queryable | Adds the field to UserQuery . | #[api_model(queryable)] |
action | Defines actions (e.g., POST requests) with optional custom parameters. | #[api_model(action = signup)] |
read_action | Adds the field to UserQuery . | #[api_model(read_action = read_data)] |
query_action | Adds the field to UserQuery . | #[api_model(query_action = list_data)] |
action_by_id | Defines actions targeted by entity ID (e.g., update, delete). | #[api_model(action_by_id = [get_data, update_data])] |
primary_key | Marks the field as the primary key. | #[api_model(primary_key)] |
nullable | Allows the database field to accept null values. | #[api_model(nullable)] |
skip | Excludes the field from serialization and database handling. | #[api_model(skip)] |
type | Describe type of SQL explicitly. | #[api_model(type = INTEGER)] |
nested | Defines nested data structures within the model. | #[api_model(nested)] |
one_to_many | Specifies a one-to-many relationship. | #[api_model(one_to_many = "posts", foreign_key = "author_id")] |
many_to_one | Specifies a many-to-one relationship. | #[api_model(many_to_one = "user")] |
many_to_many | Defines a many-to-many relationship via a join table . | #[api_model(many_to_many = "posts_tags", foreign_table_name = "tags")] |
aggregator | Embeds aggregation logic (sum, avg, etc.) into model fields. | #[api_model(aggregator = sum(price))] |
foreign_key | Indicates related key field name for one_to_many or many_to_one | #[api_model(one_to_many = "posts", foreign_key = "author_id")] |
foreign_table_name | Means foreign table name via joined table | #[api_model(many_to_many = "posts_tags", foreign_table_name = "tags")] |
foreign_primary_key | Indicates field name mapped to id field of foreign table in joined table | #[api_model(foreign_primary_key = tag_id)] |
foreign_reference_key | Indicates field name mapped to id field of this table in joined table | #[api_model(foreign_reference_key = post_id)] |
⚙️ Practical Struct Example:
Here's a complete, clearly structured Rust model:
#![allow(unused)] fn main() { #[api_model( base = "/v1/users", table = "users", queryable = [(sort = Sorter)], action = [signup(code = String, param = u64), login(test = Vec<String>)], read_action = read_data, action_by_id = [get_data, update_data], response = [signup(UserResponse)] )] #[derive(Debug, Clone, Default)] pub struct User { #[api_model(primary_key)] pub id: i64, pub username: String, #[api_model(nullable)] pub email: Option<String>, #[api_model(skip)] pub password_hash: String, } }
This setup ensures:
- Clear alignment of API paths and database table structures.
- Automatic generation of API clients and database CRUD operations.
- Custom request and response handling for specified actions.
✅ Recommended Best Practices
- Clearly define your
base
attribute to reflect your API structure. - Always explicitly specify the associated
table
for clarity and database operations. - Define custom parameters and response types for actions explicitly when required.
Following these best practices ensures your models are consistent, maintainable, and intuitive within the Biyard Rust ecosystem.
📌 Generated Summary Structure (UserSummary
):
The macro identifies fields marked with #[api_model(summary)]
and generates a simplified struct called UserSummary
:
🚩 Automatically Generated Struct:
#![allow(unused)] fn main() { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct UserSummary { pub id: i64, pub created_at: i64, pub updated_at: i64, pub nickname: Option<String>, } }
🎯 Purpose of the Summary Struct:
The UserSummary
structure is designed to provide concise, efficient, and safe data exchange:
- API List Responses: Clearly optimized for list-based API responses (e.g., listing users without sensitive data).
- Performance: Reduces payload size and overhead by excluding non-summary fields.
- Security: Automatically excludes sensitive fields (e.g.,
email
,password
) not explicitly marked for summary.
📖 Rules for Generating Summary Structures:
- Fields must explicitly include the
summary
attribute to appear in the summary structure. - The original model struct remains unaffected; the summary struct is an additional, simplified representation.
- Useful for API endpoints designed specifically for listing or simplified views.
✅ Recommended Best Practices:
- Clearly select summary fields carefully to exclude sensitive information.
- Leverage summary structures for efficient API responses.
- Regularly verify and adjust summary fields based on API usage patterns.
Following these best practices ensures safe, performant, and clear API data exchange within your Biyard Rust ecosystem.
Define API Client
The api_model
macro provides a convenient way to generate HTTP API clients based on the specified base
attribute, facilitating seamless frontend-backend interactions.
Definition of API Client
Pre-requisites
Define a Result type in crate root
In the root of the common
crate, implement a generic Result
type:
#![allow(unused)] fn main() { pub type Result<T> = std::result::Result<T, crate::error::Error>; }
Define an Error type
The custom Error
type must implement the following traits. For more details, refer to Error Handling.
Trait | Description |
---|---|
From<sqlx::Error> | Database error handling |
From<reqwest::Error> | HTTP request error handling |
From<gloo_net::Error> | HTTP request(Client) error handling |
IntoResponse | Converts errors into HTTP responses |
serde::Serialize | Serialization support |
serde::Deserialize | Deserialization support |
schemars::JsonSchema for server | JSON schema generation for documentation |
aide::OperationIo for server | API documentation generation |
Using api_model
Below is a basic definition for generating an API client:
#![allow(unused)] fn main() { use bdk::prelude::*; #[api_model(base = "/v1/users")] pub struct User { pub id: i64, pub created_at: i64, pub username: String, pub password: String, } }
- Generates a static method
get_client(endpoint: &str)
returningUserClient
.
Basic Usage
#[tokio::main] async fn main() -> Result<()> { let cli = User::get_client("https://api.example.com"); let user_id = 6; // GET /v1/users/6 let user = cli.get(user_id).await?; // GET /v1/users?size=10 let users = cli.query(UserQuery::new(10)).await?; Ok(()) }
Using summary
Fields annotated with summary
generate a simplified summary type (UserSummary
) to exclude sensitive information.
#![allow(unused)] fn main() { use bdk::prelude::*; #[api_model(base = "/v1/users")] pub struct User { #[api_model(summary)] pub id: i64, #[api_model(summary)] pub created_at: i64, #[api_model(summary)] pub username: String, pub password: String, } }
Usage
Query responses use UserSummary
:
#![allow(unused)] fn main() { #[derive(Debug, serde::Serialize, PartialEq, Eq, serde::Deserialize)] #[cfg_attr(feature = "server", derive(schemars::JsonSchema, aide::OperationIo))] pub struct UserSummary { pub id: i64, pub created_at: i64, pub username: String, } }
API actions
api_model
supports conventional API actions.
Queryable
Adding queryable
enables fields as query parameters.
#![allow(unused)] fn main() { #[api_model(base = "/v1/users")] pub struct User { #[api_model(summary, queryable)] pub username: String, } }
Usage
#![allow(unused)] fn main() { cli.query(UserQuery::new(10).with_username("name".to_string())).await?; }
Generated Query Struct:
#![allow(unused)] fn main() { pub struct UserQuery { pub size: usize, pub bookmark: Option<String>, pub username: Option<String>, } }
Query Actions
Custom actions for queries:
#![allow(unused)] fn main() { #[api_model(summary, query_action = search_by)] pub email: String, }
Usage
#![allow(unused)] fn main() { cli.query(UserQuery::new(10).with_page(1).search_by("a@example.com".to_string())).await?; }
Generated Struct:
#![allow(unused)] fn main() { pub struct UserQuery { pub size: usize, pub action: Option<UserQueryActionType>, pub bookmark: Option<String>, pub username: Option<String>, pub email: Option<String>, } }
Read Actions
Custom read actions that return a single model:
#![allow(unused)] fn main() { #[api_model(summary, queryable, read_action = find_by)] pub username: String, }
Usage
#![allow(unused)] fn main() { cli.find_by("name".to_string(), "a@example.com".to_string()).await?; }
Generated Structs:
#![allow(unused)] fn main() { pub struct UserReadAction { pub action: UserReadActionType, pub username: Option<String>, pub email: Option<String>, } pub enum UserParam { Query(UserQuery), Read(UserReadAction), } pub enum UserGetResponse { Query(QueryResponse<UserSummary>), Read(User), } }
Actions
Actions (signup
, login
) perform operations via POST
requests:
#![allow(unused)] fn main() { #[api_model(action = [signup, login])] pub password: String, }
Usage
#![allow(unused)] fn main() { cli.signup("name".to_string(), "hash".to_string(), "a@example.com".to_string()).await?; cli.login("name".to_string(), "hash".to_string()).await?; }
Generated Structs:
#![allow(unused)] fn main() { pub enum UserAction { Signup(UserSignupRequest), Login(UserLoginRequest), } pub struct UserSignupRequest { pub username: String, pub password: String, pub email: String, } pub struct UserLoginRequest { pub username: String, pub password: String, } }
Action by id
Similar to action
, but uses entity IDs in requests (PUT
, DELETE
methods):
#![allow(unused)] fn main() { #[api_model(action_by_id = [get_data, update_data])] pub email: String, }
Usage
#![allow(unused)] fn main() { cli.update_data(user_id, "a@example.com".to_string()).await?; }
Generated Structs:
#![allow(unused)] fn main() { pub enum UserActionById { GetData, UpdateData(UserUpdateRequest), } pub struct UserUpdateRequest { pub email: String, } }
Generated structures
Structure | Description |
---|---|
User | Core data model |
UserClient | API client for making HTTP calls |
UserQuery | Struct for building query parameters |
UserParam | Enum for query or read actions |
UserGetResponse | Enum representing query/read responses |
UserSummary | Simplified struct for listing responses |
UserReadAction | Struct for single entity read actions |
UserAction | Enum defining general POST actions |
UserActionById | Enum defining ID-based actions |
Repository
A Repository encapsulates the logic required to access, modify, and manage data stored in a database. It abstracts database operations into clearly defined methods, enhancing readability, maintainability, and efficiency.
Defining API Model
#![allow(unused)] fn main() { #[api_model(base = "/v1/users", table = "users")] pub struct User { #[api_model(primary_key)] pub id: i64, #[api_model(summary, read_action = get_by_name, act_by_id = update_name)] pub username: String, pub password: String, } }
Generated key structures
It generates structures necessary for database operations and API interactions:
Structure | Description |
---|---|
User | Core database model |
UserRepository | Handles CRUD operations for User |
UserRepositoryUpdateRequest | Struct for defining update requests |
User Methods
Method | Description |
---|---|
get_repository | Creates a repository instance for the model. |
UserRepository Methods
Method | Description |
---|---|
create_this_table | Creates the model's database table only. |
create_table | Creates the model's database table and related tables. |
insert | Inserts a new record into the database. |
update | Updates an existing record based on provided parameters. |
delete | Deletes a record by its ID. |
insert_with_tx | Inserts records within a transaction context. |
update_with_tx | Updates records within a transaction context. |
delete_with_tx | Deletes records within a transaction context. |
find | Retrieves multiple records based on query parameters. |
find_one | Retrieves a single record based on specific query parameters. |
Using Repository
Getting Started
Initialize the PostgreSQL pool before using the repository:
#![allow(unused)] fn main() { let pool: sqlx::Pool<sqlx::Postgres> = sqlx::postgres::PgPoolOptions::new() .max_connections(5) .connect( option_env!("DATABASE_URL") .unwrap_or("postgres://postgres:postgres@localhost:5432/test"), ) .await .unwrap(); }
Creating Tables
Create tables required by your model:
#![allow(unused)] fn main() { let repo = User::get_repository(pool); repo.create_table().await?; }
Inserting Data
Basic insert operation:
#![allow(unused)] fn main() { let username = "name".to_string(); let repo = User::get_repository(pool); let user = repo.insert(username).await?; }
Insert with transaction:
#![allow(unused)] fn main() { let mut tx = repo.pool.begin().await?; for i in 0..3 { repo.insert_with_tx(&mut *tx, format!("user {i}")).await?; } tx.commit().await?; }
Updating Data
Basic Updates
Update using predefined action:
#![allow(unused)] fn main() { let id = 1; let request = UserByIdAction::UpdateName(UserUpdateNameRequest { name: "new_name".to_string(), }); let repo = User::get_repository(pool); let param = request.into(); let user = repo.update(id, param).await?; }
Customizing Update Requests
Customizing updates with builder methods:
#![allow(unused)] fn main() { let id = 1; let request = UserByIdAction::UpdateName(UserUpdateNameRequest { name: "new_name".to_string(), }); let repo = User::get_repository(pool); let param: UserRepositoryUpdateRequest = request.into(); let param = param.with_password("new_password".to_string()); let user = repo.update(id, param).await?; }
Deleting Data
Basic delete operation:
#![allow(unused)] fn main() { let repo = User::get_repository(pool); let user = repo.delete(1).await?; }
Delete using transaction:
#![allow(unused)] fn main() { let mut tx = repo.pool.begin().await?; let user = repo.delete_with_tx(&mut *tx, 1).await?.ok_or(Error::NoUser)?; tx.commit().await?; }
Retrieving Data
Repository provides two straightforward retrieval methods: find_one
and find
.
- Recommended usage for simple
AND
-Equals
queries based onReadAction
orQuery
. - For complex queries, use QueryBuilder.
Retrieve Single Record
To fetch a single record matching a specific query:
#![allow(unused)] fn main() { let username = "name".to_string(); let request = UserReadAction::new().get_by_name(username); let repo = User::get_repository(pool); let user = repo.find_one(&request).await?; }
This retrieves a single User
instance that matches the provided query (username
).
Retrieve Multiple Records
To fetch multiple records with pagination:
#![allow(unused)] fn main() { let request = UserQuery::new(5).with_page(1); let repo = User::get_repository(pool); let users = repo.find(&request).await?; }
This retrieves multiple UserSummary
records based on the specified query (size
and page
). Useful for paginated responses and listings.
Query Builder
The Query Builder is a powerful tool for constructing database queries in a type-safe and fluent manner. It's automatically generated for models marked with the #[api_model]
attribute when the server
feature is enabled. The builder provides a chainable interface for constructing complex queries with conditions, ordering, and other database operations.
Getting Started
To use the Query Builder for a model, first ensure your model is properly annotated:
#![allow(unused)] fn main() { #[api_model(base = "/v1/endpoint", table = my_models)] struct MyModel { id: u64, name: String, is_active: bool, created_at: DateTime<Utc>, } }
Then you can access the query builder through the model's query_builder() method:
#![allow(unused)] fn main() { let query = MyModel::query_builder(); }
Building Queries
Basic Query Construction
The Query Builder supports a fluent interface for constructing queries:
#![allow(unused)] fn main() { let query = MyModel::query_builder() .name_equals("John Doe".to_string()) .is_active_is_true() .order_by_created_at_desc(); }
Condition Types
The builder generates different condition methods based on the field types:
String Fields
-
For string fields (like
name
in our example), the following methods are generated: -
{field}_equals(value: String)
- Exact match -
{field}_not_equals(value: String)
- Not equal -
{field}_contains(value: String)
- Contains substring -
{field}_not_contains(value: String)
- Does not contain substring -
{field}_starts_with(value: String)
- Starts with -
{field}_not_starts_with(value: String)
- Does not start with -
{field}_ends_with(value: String)
- Ends with -
{field}_not_ends_with(value: String)
- Does not end with
Example:
#![allow(unused)] fn main() { query.name_contains("John") // Finds records where name contains "John" .name_not_equals("Admin"); // And name is not exactly "Admin" }
Boolean Fields
For boolean fields (like is_active
):
-
{field}_is_true()
- Field is true -
{field}_is_false()
- Field is false
Example:
#![allow(unused)] fn main() { query.is_active_is_true(); // Only active records }
Ordering Results
You can order results using the generated ordering methods:
-
order_by_{field}_asc()
- Ascending order -
order_by_{field}_desc()
- Descending order
Multiple ordering conditions can be chained:
#![allow(unused)] fn main() { query.order_by_created_at_desc() .order_by_name_asc(); // Primary sort by created_at (newest first), secondary by name }
Executing Queries
After building a query, you typically execute it through the repository:
#![allow(unused)] fn main() { let results = MyModel::get_repository() .find_by_query(query) .await?; }
Advanced Usage
Combining Conditions
Conditions are combined with AND logic by default. For more complex queries, you can build subqueries:
#![allow(unused)] fn main() { let active_users = MyModel::query_builder() .is_active_is_true(); let recent_active_users = active_users .created_at_greater_than(Utc::now() - Duration::days(30)); }
Best Practices
-
Chain methods fluently for readability
-
Reuse query builders for similar queries
-
Use specific conditions rather than overly broad ones
-
Consider indexing for frequently queried fields
-
Log queries during development for debugging
The Query Builder provides a balance between type safety and flexibility, making it easier to construct database queries while reducing the risk of SQL injection and runtime errors.
Relations
In Biyard Rust projects, data models frequently represent complex relationships between database entities. The api_model
macro simplifies managing these relations, automatically generating the necessary database queries and handling serialization seamlessly.
This section describes how to clearly define and manage relational structures within your data models, supporting various relational scenarios such as one-to-many, many-to-one, and many-to-many.
Supported Relation Types
Biyard Rust projects explicitly support the following relation types:
-
One to Many Represents a relationship where one entity relates to multiple entities. For example, one user can have many posts.
-
Many to Many Represents a relationship where multiple entities can associate with multiple other entities through a join table. For example, posts associated with multiple tags.
Defining Relations with api_model
Relations are defined directly within your model structs using attributes provided by the api_model
macro:
Example
#![allow(unused)] fn main() { use bdk::prelude::*; #[api_model(base = "/v1/users", table = users)] pub struct User { #[api_model(primary_key)] pub id: i64, pub username: String, // One-to-many relation: a user has multiple posts #[api_model(one_to_many = posts, foreign_key = author_id)] pub posts: Vec<Post>, } #[api_model(base = "/v1/posts", table = posts)] pub struct Post { #[api_model(primary_key)] pub id: i64, pub title: String, // Many-to-one relation: a post belongs to one user #[api_model(many_to_one = users)] pub author_id: i64, // Many-to-many relation: a post has multiple tags #[api_model(many_to_many = posts_tags, foreign_table_name = tags, foreign_primary_key = tag_id, foreign_reference_key = post_id)] pub tags: Vec<Tag>, } #[api_model(base = "/v1/tags", table = tags)] pub struct Tag { #[api_model(primary_key)] pub id: i64, #[api_model(unique)] pub tag: String, // Many-to-many relation: a tag has multiple posts #[api_model(many_to_many = posts_tags, foreign_table_name = posts, foreign_primary_key = post_id, foreign_reference_key = tag_id)] pub posts: Vec<Post>, } }
One to Many Relationship
This document describes how to define and manage One to Many relationships in Biyard Rust projects using the api_model
macro.
Overview
A One to Many relationship occurs when a single entity can relate to multiple entities.
For example, a single User
can have multiple Post
entities associated with it.
Basic Usage
One to Many relationships are defined clearly using attributes provided by the api_model
macro.
Example
Here's a practical example where a User
has multiple related Post
entities:
#![allow(unused)] fn main() { use bdk::prelude::*; #[api_model(base = "/v1/users", table = users)] pub struct User { #[api_model(primary_key)] pub id: i64, pub username: String, // Define a One to Many relationship: a user has multiple posts #[api_model(one_to_many = posts, foreign_key = author_id)] pub posts: Vec<Post>, } #[api_model(base = "/v1/posts", table = posts)] pub struct Post { #[api_model(primary_key)] pub id: i64, pub title: String, // Define Many to One relationship: each post belongs to a single user #[api_model(many_to_one = users)] pub author_id: i64, } }
Important: The
one_to_many
attribute should always be paired with a correspondingmany_to_one
attribute to establish a clear and consistent relationship.
Attribute Descriptions
One to Many
Attribute | Description | Example |
---|---|---|
one_to_many | Name of the related table to establish the relationship | one_to_many = posts |
foreign_key | Foreign key field referencing the parent entity | foreign_key = author_id |
In the example above, the posts
field in the User
model references the author_id
field in the Post
table.
Many to One
Attribute | Description | Example |
---|---|---|
many_to_one | Name of the parent table to establish the relationship | many_to_one = users |
In the example above, the author_id
field in the Post
model references the parent User
table.
Automatically Generated Features
-
Automatic JOINs
Queries retrieving related data will automatically perform JOIN operations, simplifying data retrieval across related tables. -
Intuitive API Client and Repository
Theapi_model
macro generates appropriate methods within API clients and repositories, making related data management effortless.
Data Retrieval Examples
Here's how to retrieve related data using UserRepository
:
#![allow(unused)] fn main() { let repo = User::get_repository(pool); // Retrieve a specific user and their posts let user = repo.find_one(&UserReadAction::new().with_id(1)).await?; let posts: Vec<Post> = user.posts; }
Or using the generated API client:
#![allow(unused)] fn main() { let cli = User::get_client("https://api.example.com"); // Retrieve a user and their associated posts by user ID let user = cli.get(1).await?; let posts = user.posts; }
Recommendations and Best Practices
- Always explicitly specify the
foreign_key
when defining a One to Many relationship. - Be mindful of performance implications when retrieving large amounts of data; consider selecting specific fields or limiting query results.
- Utilize a dedicated Query Builder for optimized queries, particularly with large datasets.
- Always pair a
one_to_many
attribute with a correspondingmany_to_one
attribute to ensure relationship integrity.
Following these guidelines will ensure clear, maintainable, and efficient management of One to Many relationships in your Biyard Rust projects.
Many to Many Relationship
This document describes how to define and manage Many to Many relationships in Biyard Rust projects using the api_model
macro.
Overview
A Many to Many relationship occurs when multiple entities can relate to multiple entities through a join (pivot) table.
For example, multiple Post
entities can have multiple associated Tag
entities, and vice versa.
Basic Usage
Many to Many relationships are clearly defined using attributes provided by the api_model
macro.
Example
Here's a practical example where a Post
can have multiple related Tag
entities:
#![allow(unused)] fn main() { use bdk::prelude::*; #[api_model(base = "/v1/posts", table = posts)] pub struct Post { #[api_model(primary_key)] pub id: i64, pub title: String, // Define Many to Many relationship: a post has multiple tags #[api_model( many_to_many = posts_tags, foreign_table_name = tags, foreign_primary_key = tag_id, foreign_reference_key = post_id )] pub tags: Vec<Tag>, } #[api_model(base = "/v1/tags", table = tags)] pub struct Tag { #[api_model(primary_key)] pub id: i64, #[api_model(unique)] pub tag: String, // Define Many to Many relationship: a tag has multiple posts #[api_model( many_to_many = posts_tags, foreign_table_name = posts, foreign_primary_key = post_id, foreign_reference_key = tag_id )] pub posts: Vec<Post>, } }
Attribute Descriptions
Attribute | Description | Example |
---|---|---|
many_to_many | Name of the join (pivot) table managing the relationship | many_to_many = posts_tags |
foreign_table_name | Name of the related entity's table | foreign_table_name = tags |
foreign_primary_key | Primary key of the related entity in the join table | foreign_primary_key = tag_id |
foreign_reference_key | Primary key of the current entity in the join table | foreign_reference_key = post_id |
target_table | Determines source of relationship data (join or foreign ) | target_table = join |
In the example above, the join table posts_tags
manages the association between Post
and Tag
.
Using target_table
You can specify the source table for retrieving relationship data using the target_table
attribute. Setting target_table
to join
retrieves data from the join table, while foreign
retrieves data directly from the related entity's table.
Example
#![allow(unused)] fn main() { #[api_model( many_to_many = test_mtm_votes, type = JSONB, foreign_table_name = test_mtm_users, foreign_primary_key = user_id, foreign_reference_key = model_id, target_table = join )] pub user_votes: Vec<Vote>, #[api_model( many_to_many = test_mtm_votes, type = JSONB, foreign_table_name = test_mtm_users, foreign_primary_key = user_id, foreign_reference_key = model_id, target_table = foreign )] pub users: Vec<User>, }
In this example:
user_votes
retrieves data from the join table (test_mtm_votes
).users
retrieves data from the foreign table (test_mtm_users
).
Automatically Generated Features
-
Automatic JOINs
Queries retrieving related data will automatically perform JOIN operations across the pivot table, simplifying complex relational data retrieval. -
Intuitive API Client and Repository
Theapi_model
macro generates intuitive methods within API clients and repositories, making the handling of many-to-many relationships effortless.
Recommendations and Best Practices
- Clearly define join tables (
many_to_many
) and related keys to avoid ambiguity. - Be cautious about performance implications of many-to-many relations; optimize your queries to reduce database load.
- Consider explicitly defining and maintaining the join table structure to enhance clarity and control.
Following these guidelines will ensure clear, maintainable, and efficient management of Many to Many relationships in your Biyard Rust projects.
Nesting Relation Fields
This document explains how to use the nested
attribute to perform nested joins for retrieving related entities in Biyard Rust projects using the api_model
macro.
Overview
By default, related entities retrieved through a model are empty unless explicitly specified. To include data from nested relationships, use the nested
attribute with the generated query_builder
, enabling powerful multi-level queries.
Defining Nested Relationships
Use the nested
attribute explicitly to indicate fields that should be loaded with nested related entities.
Example
Here's an example demonstrating nested relationship definitions using User
, Post
, and Tag
:
#![allow(unused)] fn main() { use bdk::prelude::*; #[api_model(base = "/v1/users", table = users)] pub struct User { #[api_model(primary_key)] pub id: i64, pub username: String, // One-to-many nested relation: User to Posts #[api_model(one_to_many = posts, foreign_key = author_id, nested)] pub posts: Vec<Post>, } #[api_model(base = "/v1/posts", table = posts)] pub struct Post { #[api_model(primary_key)] pub id: i64, pub title: String, #[api_model(many_to_one = users)] pub author_id: i64, // Many-to-many nested relation: Posts to Tags #[api_model( many_to_many = posts_tags, foreign_table_name = tags, foreign_primary_key = tag_id, foreign_reference_key = post_id, )] pub tags: Vec<Tag>, } #[api_model(base = "/v1/tags", table = tags)] pub struct Tag { #[api_model(primary_key)] pub id: i64, #[api_model(unique)] pub tag: String, } }
Nested Queries Using Query Builder
Utilize methods on your model's query builder, such as posts_builder
or tags_builder
, to perform nested queries clearly and efficiently.
Nested Query Example
#![allow(unused)] fn main() { let result = User::query_builder() .id_equals(user_id) .posts_builder(Post::query_builder()) .query() .map(User::from) .fetch_one(&pool) .await?; }
In this query:
- The
posts_builder
method allows nested queries on theposts
field.- Conditions such as
.title_contains
optimize data retrieval.
- Conditions such as
Automatically Generated Features
-
Automatic Nested JOINs
The query builder constructs necessary JOIN statements automatically, simplifying complex data retrieval. -
Intuitive Query Builder Interface
Specify complex nested queries easily with builder methods provided by theapi_model
macro.
Recommendations and Best Practices
- Clearly specify the
nested
attribute for fields requiring nested data retrieval. - Limit deeply nested queries to essential data only to maintain performance.
- Apply precise conditions within nested builders for optimized query execution.
Following these guidelines will help ensure efficient and maintainable nested queries in your Biyard Rust projects.
Aggregator
This document explains how to define and utilize aggregator fields (sum
, max
, min
, avg
, exist
, count
) in Biyard Rust projects using the api_model
macro.
Overview
Aggregator fields allow models to include aggregated values from related entities, such as counting children or calculating sums, averages, maximums, and minimums.
Defining Aggregator Fields
You define aggregator fields directly in your models with the aggregator
attribute in the api_model
macro:
Example
Here's a practical example using a User
model and a related Post
model:
#![allow(unused)] fn main() { use bdk::prelude::*; #[api_model(base = "/v1/users", table = users)] pub struct User { #[api_model(primary_key)] pub id: i64, pub username: String, // Count of posts associated with the user #[api_model(one_to_many = posts, foreign_key = author_id, aggregator = count)] pub num_of_posts: i64, // Sum of views from posts #[api_model(one_to_many = posts, foreign_key = author_id, aggregator = sum(views))] pub total_post_views: i64, // Maximum views on a single post #[api_model(one_to_many = posts, foreign_key = author_id, aggregator = max(views))] pub max_post_views: i64, // Minimum views on a single post #[api_model(one_to_many = posts, foreign_key = author_id, aggregator = min(views))] pub min_post_views: i64, // Average views across posts #[api_model(one_to_many = posts, foreign_key = author_id, aggregator = avg(views))] pub avg_post_views: f64, // Boolean indicating if the user has any posts #[api_model(one_to_many = posts, foreign_key = author_id, aggregator = exist)] pub has_posts: bool, } #[api_model(base = "/v1/posts", table = posts)] pub struct Post { #[api_model(primary_key)] pub id: i64, pub title: String, pub views: i64, #[api_model(many_to_one = users)] pub author_id: i64, } }
Supported Aggregators
Aggregator | Description | Example | Returned Type |
---|---|---|---|
sum | Sum of a numeric field | aggregator=sum(views) | numeric |
max | Maximum value of a numeric field | aggregator=max(views) | numeric |
min | Minimum value of a numeric field | aggregator=min(views) | numeric |
avg | Average of a numeric field | aggregator=avg(views) | floating-point |
count | Count of related records | aggregator=count | integer |
exist | Checks existence of any related record | aggregator=exist | boolean |
Usage Example
Here's how you might query aggregated fields:
#![allow(unused)] fn main() { let user = User::query_builder() .id_equals(user_id) .query() .map(User::from) .fetch_one(&pool) .await?; println!("Total posts: {}", user.num_of_posts); println!("Total views: {}", user.total_post_views); println!("Max post views: {}", user.max_post_views); println!("Min post views: {}", user.min_post_views); println!("Average views per post: {:.2}", user.avg_post_views); println!("Has posts: {}", user.has_posts); }
Recommendations and Best Practices
- Clearly define the aggregator fields you need to avoid unnecessary performance overhead.
- Ensure that fields targeted by aggregators (
sum
,max
,min
,avg
) are numeric types. - Optimize your queries to avoid aggregating large datasets unnecessarily.
By following these guidelines, you can efficiently leverage aggregators in your Biyard Rust projects.
Validation
This document explains how to define and implement validation for fields in Biyard Rust projects using the api_model
macro in combination with the validator
crate.
Overview
Field validation ensures data integrity by verifying that data meets certain criteria before being processed or stored. Validation attributes defined using the validator
crate automatically integrate with generated models.
Defining Validation
Validation is defined directly within the data model structs using attributes provided by the validator.
Example
Here's a practical example demonstrating field validation on a User
model:
#![allow(unused)] fn main() { use bdk::prelude::*; use validator::Validate; #[derive(Validate)] #[api_model(base = "/v1/", table = users)] pub struct User { #[api_model(primary_key)] pub id: i64, #[validate(custom(function = "validate_nickname"))] pub nickname: String, #[validate(email)] pub email: String, #[validate(url)] pub profile_url: String, } }
Supported Validation Attributes
Use validation attributes provided by the validator
crate:
Validator Type | Description | Example |
---|---|---|
email | Validates that a field is a valid email | #[validate(email)] |
url | Validates that a field is a valid URL | #[validate(url)] |
length | Validates field length | #[validate(length(min = 1, max = 20))] |
range | Validates numeric ranges | #[validate(range(min = 1, max = 100))] |
custom | Validates using a custom function | #[validate(custom(function="validate_nickname"))] |
Custom Validation Example
Custom validation allows you to write specific validation logic:
#![allow(unused)] fn main() { use validator::ValidationError; fn validate_nickname(nickname: &str) -> Result<(), ValidationError> { if nickname.len() < 3 { return Err(ValidationError::new("nickname too short")); } Ok(()) } }
Automatic Inclusion in Generated Models
When defining validations with validator
, generated request and response structures automatically include validation checks:
#![allow(unused)] fn main() { #[tokio::main] async fn signup_user(user: User) -> Result<()> { user.validate()?; // Proceed if validation succeeds Ok(()) } }
Recommendations and Best Practices
- Define clear and concise validation rules.
- Always use specific validators rather than overly generic ones for clearer error messages.
- Leverage custom validations for complex validation logic.
Following these guidelines ensures robust and maintainable data validation in your Biyard Rust projects.
Translation
DTOs
Web
Package Structure
This document explains what elements should be included when configuring a Front Package and what functions each element performs.
Directory Structure
The package structure is as follows:
├── components
│ ├── button
│ │ ├── mod.rs
│ ├── checkbox
│ │ ├── mod.rs
│ ├── selectbox
│ │ ├── mod.rs
│ └── mod.rs
├── pages
│ ├── main
│ │ ├── controller.rs
│ │ ├── i18n.rs
│ │ ├── mod.rs
│ │ └── page.rs
│ ├── create
│ │ ├── controller.rs
│ │ ├── i18n.rs
│ │ ├── mod.rs
│ ├──├── page.rs
│ ├── mod.rs
│ ├── layout.rs
│ └── page.rs
├── services
│ ├── user_service.rs
│ └── mod.rs
├── utils
│ ├── api.rs
│ ├── time.rs
│ ├── hash.rs
│ └── mod.rs
├── main.rs
├── routes.rs
├── theme.rs
└── config.rs
Explanation of Structure
Root Files
-
main.rs
It is the entry point of the Dioxus app and is responsible for running the entire application by initializing the logger, registering services, setting themes, loading external resources, and configuring routing.
The entry point that defines the structure of a Dioxus app and integrates and manages UI layouts, routes, pages, services, components, and utility modules.Example:
pub mod config; pub mod pages; pub mod routes; pub mod service; pub mod utils; pub mod components;
-
routes.rs
Defines all page paths and language-specific routing structure within the app, and a router configuration for Dioxus that manages page navigation based on the /[:lang] namespace.Example:
use bdk::prelude::; use pages::; #[derive(Clone, Routable, Debug, PartialEq)] #[rustfmt::skip] pub enum Route { #[nest("/:lang")] #[layout(RootLayout)] #[route("/main")] MainPage { lang: Language }, #[route("/create")] CreatePage { lang: Language }, #[end_layout] #[end_nest] #[route("/:..route")] NotFoundPage { route: Vec<String> }, }
-
config.rs
A configuration module that initializes environment variable-based settings (Config) when the app runs and provides them in a globally accessible singleton form. -
theme.rs
A file that defines global theme data and manages the application of colors and font styles consistently across the UI via Context.Example:
#[derive(Debug, Clone)] pub struct ThemeData { pub active: String, pub active00: String, pub font_theme: FontTheme, } #[derive(Debug, Clone)] pub struct FontTheme { pub exbold15: String, pub bold15: String, } impl Default for FontTheme { fn default() -> Self { FontTheme { exbold15: "font-extrabold text-[15px] leading-[22.5px]".to_string(), bold15: "font-bold text-[15px] leading[22.5px]".to_string(), } } } impl Default for ThemeData { fn default() -> Self { ThemeData { active: "#68D36C".to_string(), active00: "#68D36C".to_string(), font_theme: FontTheme::default(), } } } use bdk::prelude::*; #[derive(Debug, Clone, Copy, Default)] pub struct Theme { pub data: Signal<ThemeData>, } impl Theme { pub fn init() { use_context_provider(|| Self { data: Signal::new(ThemeData::default()), }); } pub fn get_data(&self) -> ThemeData { (self.data)() } pub fn get_font_theme(&self) -> FontTheme { (self.data)().font_theme.clone() } }
components
- Create UI by organizing modules that can be commonly used for each page into a components directory.
- Components are configured by creating a directory with each component name and creating a mod.rs file inside that directory.
Example:
If the Components is defined as:
├── components
│ ├── button
│ │ ├── mod.rs
│ ├── checkbox
│ │ ├── mod.rs
│ ├── selectbox
│ │ ├── mod.rs
│ └── mod.rs
At this time, the mod.rs file should be structured as follows:
pub mod button;
pub mod checkbox;
pub mod selectbox;
services
- Configure services for functions that operate commonly within a page.
step
The steps to configure and use the service are as follows:
- service file setting
- You can define a service file as follows:
use bdk::prelude::*;
#[derive(Debug, Clone, Copy, Default)]
pub struct UserService {
pub id: Signal<Option<String>>,
}
impl UserService {
pub fn init() {
let srv = Self {
id: Signal::new(None),
};
use_context_provider(|| srv);
}
}
- Initialize in main.rs file
- Services are initialized globally in main.rs and are set up so that they can be used commonly by each page component.
- Service initialization is done as follows:
use bdk::prelude::*;
fn main() {
dioxus_logger::init(config::get().log_level).expect("failed to init logger");
dioxus_aws::launch(App);
}
#[component]
fn App() -> Element {
UserService::init();
rsx! {
Router::<Route> {}
}
}
- accesssed via use_context
- In each page's controller.rs, services are accessed via use_context, allowing for seamless dependency injection and modular usage.
- Services can be accessed as follows:
use bdk::prelude::;
#[derive(Debug, Clone, Copy, DioxusController)]
pub struct Controller {
user_service: UserService,
}
impl Controller {
pub fn new(
lang: Language,
) -> std::result::Result<Self, RenderError> {
let ctrl = Self {
user_service: use_context(),
};
Ok(ctrl)
}
}
utils
- A space where utility functions that can be commonly used on each page are collected.
- Modularize and manage general logic such as time formatting (time.rs) or hash generation (hash.rs).
Example:
If the Utils is defined as:
├── utils
│ ├── api.rs
│ ├── time.rs
│ ├── hash.rs
│ └── mod.rs
pages
- Each page directory name must match the routing name.
- Each directory consists of controller.rs, i18n.rs, mod.rs, and page.rs.
- The page mainly utilizes the MVC pattern, the model is managed in the dto package, the ui is managed in the page.rs file, and the functional logic is managed in the controller.rs file.
- Components that are only used within a page must be used by configuring a separate components directory within the directory.
Example:
If the pages is defined as:
├── main
│ ├── components
│ │ ├── mod.rs
│ │ ├── objective_quiz.rs
│ │ └── subjective_quiz.rs
│ ├── controller.rs
│ ├── i18n.rs
│ ├── mod.rs
│ └── page.rs
├── layout.rs
└── mod.rs
controller.rs
- Mainly creating functional logic.
- The pattern used is as follows:
controller.rs
use bdk::prelude::*;
#[derive(Debug, Clone, Copy, DioxusController)]
pub struct Controller {
count: Signal<i64>,
lang: Language,
}
impl Controller {
pub fn new(lang: Language) -> std::result::Result<Self, RenderError> {
let count = use_signal(|| 0);
let ctrl = Self { lang, count };
Ok(ctrl)
}
pub fn add_count(&mut self) {
let mut c = (self.count)();
c += 1;
(self.count).set(c);
}
pub fn get_count(&self) -> i64 {
(self.count)()
}
}
page.rs
use bdk::prelude::*;
#[component]
pub fn MainPage(lang: Language) -> Element {
let ctrl = Controller::new(lang)?;
let count = ctrl.get_count();
rsx! {
div {
"main page"
}
}
}
i18n.rs
- Translate configuration using the translate macro, which is an internally implemented macro.
- Basically, ko and en are supported in two versions, and the two versions are described in the i18n.rs file when configuring the UI.
- At this time, the Translation name should reflect the page name.
- Examples of usage are as follows.
i18n.rs
use bdk::prelude::*;
translate! {
MainTranslate;
text: {
ko: "메인 페이지",
en: "Main Page"
}
}
page.rs
use bdk::prelude::*;
#[component]
pub fn MainPage(lang: Language) -> Element {
let ctrl = Controller::new(lang);
let tr: MainTranslate = translate(&lang);
let count = ctrl.get_count();
let text = tr.text;
rsx! {
div {
"main page"
}
}
}
page.rs
- page.rs is defined around UI composition and screen elements shown to the user, rather than business logic.
- Business logic is configured in the controller.rs file, and translation-related parts are configured in the i18n.rs file.
Testing
Mobile App
Backend
Package Structure
Authentication
with_auth_cookie
function
The with_auth_cookie
function is used to facilitate authentication for subsequent client requests by attaching an authentication cookie to the HTTP response headers.
This function sets the SET-COOKIE
header in the HTTP response, allowing the client to store the authentication cookie for subsequent requests.
Supporting Scheme:
#![allow(unused)] fn main() { pub enum TokenScheme { Bearer, Usersig, XServerKey, Secret, } }
Example:
// api/main.rs #[tokio::main] async fn main() -> Result<()> { let app = make_app().await?; let port = config::get().port; let listener = TcpListener::bind(format!("0.0.0.0:{}", port)) .await .unwrap(); tracing::info!("listening on {}", listener.local_addr().unwrap()); let cors_layer = CorsLayer::new() .allow_origin(AllowOrigin::exact("{YOUR_CLIENT_DOMAIN}")) .allow_credentials(true) .allow_methods([Method::GET, Method::POST, Method::OPTIONS]) .allow_headers(vec![CONTENT_TYPE, AUTHORIZATION, COOKIE]); let app = app.layer(cors_layer); by_axum::serve_wo_cors_layer(listener, app).await.unwrap(); Ok(()) } // Store the generated JWT (which serves as a Bearer token) in an authentication cookie. // The TokenScheme::Bearer argument indicates the type of the token being stored, async fn login( &self, ... ) -> Result<JsonWithHeaders<User>> { // Retrieve or create a user object let user: User = /* Some logic to fetch or create the user */; // Generate a JWT token for the user let jwt = AppClaims::generate_token(&user)?; // Attach the token as a Bearer token in the authentication cookie Ok( JsonWithHeaders::new(user) .with_auth_cookie(TokenScheme::Bearer, &jwt) ) }