Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 for api.example.com.
  • Similarly, packages/www indicates implementation of www.example.com and example.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.

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 of Debug, Clone, serde::Deserialize, serde::Serialize, Default, PartialEq by default.
  • It also add schemars::JsonSchema and aide::OperationIo for server 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

AttributeDescriptionExample
baseAPI path prefix for model-related endpoints.base = "/v1/users"
tableSpecifies the database table associated with the model.table = "users"
queryableEnables query functionality with optional sorting/filtering parameters.queryable = [(sort = Sorter)]
actionDefines actions (e.g., POST requests) with optional custom parameters.action = [signup(code = String, param = u64)]
read_actionDefines custom read-only actions without parameters.read_action = read_data
action_by_idDefines actions targeted by entity ID (e.g., update, delete).action_by_id = [get_data, update_data]
responseSpecifies custom response types for defined actions.response = [signup(UserResponse)]

📖 Field Attributes

Field attributes specify behaviors at the individual field level:

AttributeDescriptionExample
summaryAdds the field to UserSummary.#[api_model(summary)]
queryableAdds the field to UserQuery.#[api_model(queryable)]
actionDefines actions (e.g., POST requests) with optional custom parameters.#[api_model(action = signup)]
read_actionAdds the field to UserQuery.#[api_model(read_action = read_data)]
query_actionAdds the field to UserQuery.#[api_model(query_action = list_data)]
action_by_idDefines actions targeted by entity ID (e.g., update, delete).#[api_model(action_by_id = [get_data, update_data])]
primary_keyMarks the field as the primary key.#[api_model(primary_key)]
nullableAllows the database field to accept null values.#[api_model(nullable)]
skipExcludes the field from serialization and database handling.#[api_model(skip)]
typeDescribe type of SQL explicitly.#[api_model(type = INTEGER)]
nestedDefines nested data structures within the model.#[api_model(nested)]
one_to_manySpecifies a one-to-many relationship.#[api_model(one_to_many = "posts", foreign_key = "author_id")]
many_to_oneSpecifies a many-to-one relationship.#[api_model(many_to_one = "user")]
many_to_manyDefines a many-to-many relationship via a join table.#[api_model(many_to_many = "posts_tags", foreign_table_name = "tags")]
aggregatorEmbeds aggregation logic (sum, avg, etc.) into model fields.#[api_model(aggregator = sum(price))]
foreign_keyIndicates related key field name for one_to_many or many_to_one#[api_model(one_to_many = "posts", foreign_key = "author_id")]
foreign_table_nameMeans foreign table name via joined table#[api_model(many_to_many = "posts_tags", foreign_table_name = "tags")]
foreign_primary_keyIndicates field name mapped to id field of foreign table in joined table#[api_model(foreign_primary_key = tag_id)]
foreign_reference_keyIndicates 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.

  • 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.

  • 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.

TraitDescription
From<sqlx::Error>Database error handling
From<reqwest::Error>HTTP request error handling
From<gloo_net::Error>HTTP request(Client) error handling
IntoResponseConverts errors into HTTP responses
serde::SerializeSerialization support
serde::DeserializeDeserialization support
schemars::JsonSchema for serverJSON schema generation for documentation
aide::OperationIo for serverAPI 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) returning UserClient.

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

StructureDescription
UserCore data model
UserClientAPI client for making HTTP calls
UserQueryStruct for building query parameters
UserParamEnum for query or read actions
UserGetResponseEnum representing query/read responses
UserSummarySimplified struct for listing responses
UserReadActionStruct for single entity read actions
UserActionEnum defining general POST actions
UserActionByIdEnum 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:

StructureDescription
UserCore database model
UserRepositoryHandles CRUD operations for User
UserRepositoryUpdateRequestStruct for defining update requests

User Methods

MethodDescription
get_repositoryCreates a repository instance for the model.

UserRepository Methods

MethodDescription
create_this_tableCreates the model's database table only.
create_tableCreates the model's database table and related tables.
insertInserts a new record into the database.
updateUpdates an existing record based on provided parameters.
deleteDeletes a record by its ID.
insert_with_txInserts records within a transaction context.
update_with_txUpdates records within a transaction context.
delete_with_txDeletes records within a transaction context.
findRetrieves multiple records based on query parameters.
find_oneRetrieves 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 on ReadAction or Query.
  • 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

  1. Chain methods fluently for readability

  2. Reuse query builders for similar queries

  3. Use specific conditions rather than overly broad ones

  4. Consider indexing for frequently queried fields

  5. 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 corresponding many_to_one attribute to establish a clear and consistent relationship.

Attribute Descriptions

One to Many

AttributeDescriptionExample
one_to_manyName of the related table to establish the relationshipone_to_many = posts
foreign_keyForeign key field referencing the parent entityforeign_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

AttributeDescriptionExample
many_to_oneName of the parent table to establish the relationshipmany_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
    The api_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 corresponding many_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

AttributeDescriptionExample
many_to_manyName of the join (pivot) table managing the relationshipmany_to_many = posts_tags
foreign_table_nameName of the related entity's tableforeign_table_name = tags
foreign_primary_keyPrimary key of the related entity in the join tableforeign_primary_key = tag_id
foreign_reference_keyPrimary key of the current entity in the join tableforeign_reference_key = post_id
target_tableDetermines 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
    The api_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 the posts field.
    • Conditions such as .title_contains optimize data retrieval.

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 the api_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

AggregatorDescriptionExampleReturned Type
sumSum of a numeric fieldaggregator=sum(views)numeric
maxMaximum value of a numeric fieldaggregator=max(views)numeric
minMinimum value of a numeric fieldaggregator=min(views)numeric
avgAverage of a numeric fieldaggregator=avg(views)floating-point
countCount of related recordsaggregator=countinteger
existChecks existence of any related recordaggregator=existboolean

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 TypeDescriptionExample
emailValidates that a field is a valid email#[validate(email)]
urlValidates that a field is a valid URL#[validate(url)]
lengthValidates field length#[validate(length(min = 1, max = 20))]
rangeValidates numeric ranges#[validate(range(min = 1, max = 100))]
customValidates 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:

  1. 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);
    }
}
  1. 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> {}
    }
}

  1. 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

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)
      )
   }

Testing

ICP Canister

Package Structure

Testing

Web

Backend

Integration Testing