This commit is contained in:
kristof 2025-04-04 11:25:24 +02:00
parent 8a5e41a265
commit 186e339740
13 changed files with 159 additions and 946 deletions

7
herodb/Cargo.lock generated
View File

@ -658,6 +658,7 @@ dependencies = [
"bincode",
"brotli",
"chrono",
"paste",
"poem",
"poem-openapi",
"rhai",
@ -1007,6 +1008,12 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "percent-encoding"
version = "2.3.1"

View File

@ -20,6 +20,7 @@ poem = "1.3.55"
poem-openapi = { version = "2.0.11", features = ["swagger-ui"] }
tokio = { version = "1", features = ["full"] }
rhai = "1.15.1"
paste = "1.0"
[[example]]
name = "rhai_demo"

81
herodb/README.md Normal file
View File

@ -0,0 +1,81 @@
# HeroDB
A database library built on top of sled with model support.
## Features
- Type-safe database operations
- Builder pattern for model creation
- Transaction support
- Model-specific convenience methods
- Compression for efficient storage
## Usage
### Basic Usage
```rust
use herodb::db::{DB, DBBuilder};
use herodb::models::biz::{Product, ProductBuilder, ProductType, ProductStatus, Currency, CurrencyBuilder};
// Create a database instance
let db = DBBuilder::new("db")
.register_model::<Product>()
.register_model::<Currency>()
.build()
.expect("Failed to create database");
// Create a product using the builder pattern
let price = CurrencyBuilder::new()
.amount(29.99)
.currency_code("USD")
.build()
.expect("Failed to build currency");
let product = ProductBuilder::new()
.id(1)
.name("Premium Service")
.description("Our premium service offering")
.price(price)
.type_(ProductType::Service)
.category("Services")
.status(ProductStatus::Available)
.max_amount(100)
.validity_days(30)
.build()
.expect("Failed to build product");
// Insert the product using the generic method
db.set(&product).expect("Failed to insert product");
// Retrieve the product
let retrieved_product = db.get::<Product>(&"1".to_string()).expect("Failed to retrieve product");
```
### Using Model-Specific Convenience Methods
The library provides model-specific convenience methods for common database operations:
```rust
// Insert a product using the model-specific method
db.insert_product(&product).expect("Failed to insert product");
// Retrieve a product by ID
let retrieved_product = db.get_product(1).expect("Failed to retrieve product");
// List all products
let all_products = db.list_products().expect("Failed to list products");
// Delete a product
db.delete_product(1).expect("Failed to delete product");
```
These methods are available for all registered models:
- `insert_product`, `get_product`, `delete_product`, `list_products` for Product
- `insert_currency`, `get_currency`, `delete_currency`, `list_currencies` for Currency
- `insert_sale`, `get_sale`, `delete_sale`, `list_sales` for Sale
## License
MIT

29
herodb/src/db/macros.rs Normal file
View File

@ -0,0 +1,29 @@
/// Macro to implement typed access methods on the DB struct for a given model
#[macro_export]
macro_rules! impl_model_methods {
($model:ty, $singular:ident, $plural:ident) => {
impl DB {
paste::paste! {
/// Insert a model instance into the database
pub fn [<insert_ $singular>](&self, item: &$model) -> SledDBResult<()> {
self.set(item)
}
/// Get a model instance by its ID
pub fn [<get_ $singular>](&self, id: u32) -> SledDBResult<$model> {
self.get::<$model>(&id.to_string())
}
/// Delete a model instance by its ID
pub fn [<delete_ $singular>](&self, id: u32) -> SledDBResult<()> {
self.delete::<$model>(&id.to_string())
}
/// List all model instances
pub fn [<list_ $plural>](&self) -> SledDBResult<Vec<$model>> {
self.list::<$model>()
}
}
}
};
}

View File

@ -1,6 +1,7 @@
pub mod base;
pub mod db;
pub mod macros;
pub mod model_methods;
// Re-export everything needed at the module level
pub use base::{SledDB, SledDBError, SledDBResult, Storable, SledModel};
pub use db::{DB, DBBuilder, ModelRegistration, SledModelRegistration};
pub use db::{DB, DBBuilder};

View File

@ -0,0 +1,15 @@
use crate::db::db::DB;
use crate::db::base::{SledDBResult, SledModel};
use crate::impl_model_methods;
use crate::models::biz::product::Product;
use crate::models::biz::sale::Sale;
use crate::models::biz::Currency;
// Implement model-specific methods for Product
impl_model_methods!(Product, product, products);
// Implement model-specific methods for Sale
impl_model_methods!(Sale, sale, sales);
// Implement model-specific methods for Currency
impl_model_methods!(Currency, currency, currencies);

View File

@ -6,10 +6,6 @@
// Core modules
mod db;
mod error;
pub mod server;
// Domain-specific modules
pub mod zaz;
// Re-exports
pub use error::Error;

View File

@ -243,9 +243,27 @@ let mut sale = SaleBuilder::new()
sale.update_status(SaleStatus::Completed);
```
## Benefits of the Builder Pattern
## Database Operations
The library provides model-specific convenience methods for common database operations:
```rust
// Insert a product
db.insert_product(&product).expect("Failed to insert product");
// Retrieve a product by ID
let retrieved_product = db.get_product(1).expect("Failed to retrieve product");
// List all products
let all_products = db.list_products().expect("Failed to list products");
// Delete a product
db.delete_product(1).expect("Failed to delete product");
```
These methods are available for all root objects:
- `insert_product`, `get_product`, `delete_product`, `list_products` for Product
- `insert_currency`, `get_currency`, `delete_currency`, `list_currencies` for Currency
- `insert_sale`, `get_sale`, `delete_sale`, `list_sales` for Sale
- You don't need to remember the order of parameters.
- You get readable, self-documenting code.
- It's easier to provide defaults or optional values.
- Validation happens at build time, ensuring all required fields are provided.

View File

@ -1,368 +0,0 @@
//! API module for the HeroDB server
use crate::core::DB;
use crate::server::models::{ApiError, SuccessResponse, UserCreate, UserUpdate, SaleCreate, SaleStatusUpdate,
UserResponse, SuccessOrError
};
use crate::zaz::create_zaz_db;
use crate::zaz::models::*;
use crate::zaz::models::sale::{SaleStatus, SaleItem};
use crate::zaz::models::product::Currency;
use poem_openapi::{
param::Path,
payload::Json,
OpenApi,
};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use chrono::Utc;
/// API handler struct that holds the database connection
pub struct Api {
db: Arc<Mutex<DB>>,
}
impl Api {
/// Create a new API instance with the given database path
pub fn new(db_path: PathBuf) -> Self {
// Create the DB
let db = match create_zaz_db(db_path) {
Ok(db) => db,
Err(e) => {
eprintln!("Failed to create DB: {}", e);
panic!("Failed to initialize database");
}
};
// Wrap in Arc<Mutex> for thread safety
Self {
db: Arc::new(Mutex::new(db)),
}
}
}
/// OpenAPI implementation for the API
#[OpenApi]
impl Api {
/// Get all users
#[oai(path = "/users", method = "get")]
async fn get_users(&self) -> Json<String> {
let db = self.db.lock().unwrap();
match db.list::<User>() {
Ok(users) => {
// Convert to JSON manually
let json_result = serde_json::to_string(&users).unwrap_or_else(|_| "[]".to_string());
Json(json_result)
}
Err(e) => {
eprintln!("Error listing users: {}", e);
Json("[]".to_string())
}
}
}
/// Get a user by ID
#[oai(path = "/users/:id", method = "get")]
async fn get_user(&self, id: Path<u32>) -> UserResponse {
let db = self.db.lock().unwrap();
match db.get::<User>(&id.0.to_string()) {
Ok(user) => {
// Convert to JSON manually
let json_result = serde_json::to_string(&user).unwrap_or_else(|_| "{}".to_string());
UserResponse::Ok(Json(json_result))
}
Err(e) => {
eprintln!("Error getting user: {}", e);
UserResponse::NotFound(Json(ApiError::not_found(id.0)))
}
}
}
/// Create a new user
#[oai(path = "/users", method = "post")]
async fn create_user(
&self,
user: Json<UserCreate>,
) -> UserResponse {
let db = self.db.lock().unwrap();
// Find the next available ID
let users: Vec<User> = match db.list() {
Ok(users) => users,
Err(e) => {
eprintln!("Error listing users: {}", e);
return UserResponse::InternalError(Json(ApiError::internal_error("Failed to generate ID")));
}
};
let next_id = users.iter().map(|u| u.id).max().unwrap_or(0) + 1;
// Create the new user
let new_user = User::new(
next_id,
user.name.clone(),
user.email.clone(),
user.password.clone(),
user.company.clone(),
user.role.clone(),
);
// Save the user
match db.set(&new_user) {
Ok(_) => {
let json_result = serde_json::to_string(&new_user).unwrap_or_else(|_| "{}".to_string());
UserResponse::Ok(Json(json_result))
},
Err(e) => {
eprintln!("Error creating user: {}", e);
UserResponse::InternalError(Json(ApiError::internal_error("Failed to create user")))
}
}
}
/// Update a user
#[oai(path = "/users/:id", method = "put")]
async fn update_user(
&self,
id: Path<u32>,
user: Json<UserUpdate>,
) -> UserResponse {
let db = self.db.lock().unwrap();
// Get the existing user
let existing_user: User = match db.get(&id.0.to_string()) {
Ok(user) => user,
Err(e) => {
eprintln!("Error getting user: {}", e);
return UserResponse::NotFound(Json(ApiError::not_found(id.0)));
}
};
// Update the user
let updated_user = User {
id: existing_user.id,
name: user.name.clone().unwrap_or(existing_user.name),
email: user.email.clone().unwrap_or(existing_user.email),
password: user.password.clone().unwrap_or(existing_user.password),
company: user.company.clone().unwrap_or(existing_user.company),
role: user.role.clone().unwrap_or(existing_user.role),
created_at: existing_user.created_at,
updated_at: Utc::now(),
};
// Save the updated user
match db.set(&updated_user) {
Ok(_) => {
let json_result = serde_json::to_string(&updated_user).unwrap_or_else(|_| "{}".to_string());
UserResponse::Ok(Json(json_result))
},
Err(e) => {
eprintln!("Error updating user: {}", e);
UserResponse::InternalError(Json(ApiError::internal_error("Failed to update user")))
}
}
}
/// Delete a user
#[oai(path = "/users/:id", method = "delete")]
async fn delete_user(&self, id: Path<u32>) -> SuccessOrError {
let db = self.db.lock().unwrap();
match db.delete::<User>(&id.0.to_string()) {
Ok(_) => SuccessOrError::Ok(Json(SuccessResponse {
success: true,
message: format!("User with ID {} deleted", id.0),
})),
Err(e) => {
eprintln!("Error deleting user: {}", e);
SuccessOrError::NotFound(Json(ApiError::not_found(id.0)))
}
}
}
/// Get all products
#[oai(path = "/products", method = "get")]
async fn get_products(&self) -> Json<String> {
let db = self.db.lock().unwrap();
match db.list::<Product>() {
Ok(products) => {
let json_result = serde_json::to_string(&products).unwrap_or_else(|_| "[]".to_string());
Json(json_result)
}
Err(e) => {
eprintln!("Error listing products: {}", e);
Json("[]".to_string())
}
}
}
/// Get a product by ID
#[oai(path = "/products/:id", method = "get")]
async fn get_product(&self, id: Path<u32>) -> UserResponse {
let db = self.db.lock().unwrap();
match db.get::<Product>(&id.0.to_string()) {
Ok(product) => {
let json_result = serde_json::to_string(&product).unwrap_or_else(|_| "{}".to_string());
UserResponse::Ok(Json(json_result))
}
Err(e) => {
eprintln!("Error getting product: {}", e);
UserResponse::NotFound(Json(ApiError::not_found(id.0)))
}
}
}
/// Get all sales
#[oai(path = "/sales", method = "get")]
async fn get_sales(&self) -> Json<String> {
let db = self.db.lock().unwrap();
match db.list::<Sale>() {
Ok(sales) => {
let json_result = serde_json::to_string(&sales).unwrap_or_else(|_| "[]".to_string());
Json(json_result)
}
Err(e) => {
eprintln!("Error listing sales: {}", e);
Json("[]".to_string())
}
}
}
/// Get a sale by ID
#[oai(path = "/sales/:id", method = "get")]
async fn get_sale(&self, id: Path<u32>) -> UserResponse {
let db = self.db.lock().unwrap();
match db.get::<Sale>(&id.0.to_string()) {
Ok(sale) => {
let json_result = serde_json::to_string(&sale).unwrap_or_else(|_| "{}".to_string());
UserResponse::Ok(Json(json_result))
}
Err(e) => {
eprintln!("Error getting sale: {}", e);
UserResponse::NotFound(Json(ApiError::not_found(id.0)))
}
}
}
/// Create a new sale
#[oai(path = "/sales", method = "post")]
async fn create_sale(
&self,
sale: Json<SaleCreate>,
) -> UserResponse {
let db = self.db.lock().unwrap();
// Find the next available ID
let sales: Vec<Sale> = match db.list() {
Ok(sales) => sales,
Err(e) => {
eprintln!("Error listing sales: {}", e);
return UserResponse::InternalError(Json(ApiError::internal_error("Failed to generate ID")));
}
};
let next_id = sales.iter().map(|s| s.id).max().unwrap_or(0) + 1;
// Create the new sale
let mut new_sale = Sale::new(
next_id,
sale.company_id,
sale.buyer_name.clone(),
sale.buyer_email.clone(),
sale.currency_code.clone(),
SaleStatus::Pending,
);
// Add items if provided
if let Some(items) = &sale.items {
for (i, item) in items.iter().enumerate() {
let item_id = (i + 1) as u32;
let active_till = Utc::now() + chrono::Duration::days(365); // Default 1 year
let sale_item = SaleItem::new(
item_id,
next_id,
item.product_id,
item.name.clone(),
item.quantity,
Currency {
amount: item.unit_price,
currency_code: sale.currency_code.clone(),
},
active_till,
);
new_sale.add_item(sale_item);
}
}
// Save the sale
match db.set(&new_sale) {
Ok(_) => {
let json_result = serde_json::to_string(&new_sale).unwrap_or_else(|_| "{}".to_string());
UserResponse::Ok(Json(json_result))
}
Err(e) => {
eprintln!("Error creating sale: {}", e);
UserResponse::InternalError(Json(ApiError::internal_error("Failed to create sale")))
}
}
}
/// Update a sale status
#[oai(path = "/sales/:id/status", method = "put")]
async fn update_sale_status(
&self,
id: Path<u32>,
status: Json<SaleStatusUpdate>,
) -> UserResponse {
let db = self.db.lock().unwrap();
// Get the existing sale
let mut existing_sale: Sale = match db.get(&id.0.to_string()) {
Ok(sale) => sale,
Err(e) => {
eprintln!("Error getting sale: {}", e);
return UserResponse::NotFound(Json(ApiError::not_found(id.0)));
}
};
// Parse and update the status
let new_status = match status.parse_status() {
Ok(status) => status,
Err(e) => return UserResponse::InternalError(Json(e)),
};
// Update the status
existing_sale.update_status(new_status);
// Save the updated sale
match db.set(&existing_sale) {
Ok(_) => {
let json_result = serde_json::to_string(&existing_sale).unwrap_or_else(|_| "{}".to_string());
UserResponse::Ok(Json(json_result))
}
Err(e) => {
eprintln!("Error updating sale: {}", e);
UserResponse::InternalError(Json(ApiError::internal_error("Failed to update sale")))
}
}
}
/// Delete a sale
#[oai(path = "/sales/:id", method = "delete")]
async fn delete_sale(&self, id: Path<u32>) -> SuccessOrError {
let db = self.db.lock().unwrap();
match db.delete::<Sale>(&id.0.to_string()) {
Ok(_) => SuccessOrError::Ok(Json(SuccessResponse {
success: true,
message: format!("Sale with ID {} deleted", id.0),
})),
Err(e) => {
eprintln!("Error deleting sale: {}", e);
SuccessOrError::NotFound(Json(ApiError::not_found(id.0)))
}
}
}
}

View File

@ -1,289 +0,0 @@
//! Extensions to make the zaz models compatible with OpenAPI
//!
//! This module adds the necessary traits and implementations to make the
//! existing zaz models work with OpenAPI.
use poem_openapi::types::{ToSchema, Type};
use chrono::{DateTime, Utc};
use crate::zaz::models::*;
// Make DateTime<Utc> compatible with OpenAPI
impl Type for DateTime<Utc> {
const IS_REQUIRED: bool = true;
type RawValueType = String;
type RawElementValueType = Self::RawValueType;
fn name() -> std::borrow::Cow<'static, str> {
"DateTime".into()
}
fn schema_ref() -> std::borrow::Cow<'static, str> {
"string".into()
}
fn as_raw_value(&self) -> Option<Self::RawValueType> {
Some(self.to_rfc3339())
}
fn raw_element_iter<'a>(
&'a self,
) -> Box<dyn Iterator<Item = Self::RawElementValueType> + 'a> {
Box::new(self.as_raw_value().into_iter())
}
}
// Make Currency compatible with OpenAPI
impl Type for Currency {
const IS_REQUIRED: bool = true;
type RawValueType = serde_json::Value;
type RawElementValueType = Self::RawValueType;
fn name() -> std::borrow::Cow<'static, str> {
"Currency".into()
}
fn schema_ref() -> std::borrow::Cow<'static, str> {
"object".into()
}
fn as_raw_value(&self) -> Option<Self::RawValueType> {
Some(serde_json::json!({
"amount": self.amount,
"currency_code": self.currency_code
}))
}
fn raw_element_iter<'a>(
&'a self,
) -> Box<dyn Iterator<Item = Self::RawElementValueType> + 'a> {
Box::new(self.as_raw_value().into_iter())
}
}
// Make SaleStatus compatible with OpenAPI
impl Type for SaleStatus {
const IS_REQUIRED: bool = true;
type RawValueType = String;
type RawElementValueType = Self::RawValueType;
fn name() -> std::borrow::Cow<'static, str> {
"SaleStatus".into()
}
fn schema_ref() -> std::borrow::Cow<'static, str> {
"string".into()
}
fn as_raw_value(&self) -> Option<Self::RawValueType> {
Some(match self {
SaleStatus::Pending => "pending".to_string(),
SaleStatus::Completed => "completed".to_string(),
SaleStatus::Cancelled => "cancelled".to_string(),
})
}
fn raw_element_iter<'a>(
&'a self,
) -> Box<dyn Iterator<Item = Self::RawElementValueType> + 'a> {
Box::new(self.as_raw_value().into_iter())
}
}
// Schema generation for User
impl ToSchema for User {
fn schema() -> poem_openapi::registry::MetaSchema {
let mut schema = poem_openapi::registry::MetaSchema::new("User");
schema.properties.insert(
"id".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(u32::schema())),
);
schema.properties.insert(
"name".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(String::schema())),
);
schema.properties.insert(
"email".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(String::schema())),
);
schema.properties.insert(
"company".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(String::schema())),
);
schema.properties.insert(
"role".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(String::schema())),
);
schema.properties.insert(
"created_at".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<DateTime<Utc> as ToSchema>::schema(),
)),
);
schema.properties.insert(
"updated_at".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<DateTime<Utc> as ToSchema>::schema(),
)),
);
// Note: We exclude password for security reasons
schema
}
}
// Schema generation for Product
impl ToSchema for Product {
fn schema() -> poem_openapi::registry::MetaSchema {
let mut schema = poem_openapi::registry::MetaSchema::new("Product");
schema.properties.insert(
"id".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(u32::schema())),
);
schema.properties.insert(
"company_id".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(u32::schema())),
);
schema.properties.insert(
"name".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(String::schema())),
);
schema.properties.insert(
"description".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(String::schema())),
);
schema.properties.insert(
"price".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<Currency as ToSchema>::schema(),
)),
);
schema.properties.insert(
"status".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(String::schema())),
);
schema.properties.insert(
"created_at".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<DateTime<Utc> as ToSchema>::schema(),
)),
);
schema.properties.insert(
"updated_at".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<DateTime<Utc> as ToSchema>::schema(),
)),
);
schema
}
}
// Schema generation for SaleItem
impl ToSchema for SaleItem {
fn schema() -> poem_openapi::registry::MetaSchema {
let mut schema = poem_openapi::registry::MetaSchema::new("SaleItem");
schema.properties.insert(
"id".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(u32::schema())),
);
schema.properties.insert(
"sale_id".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(u32::schema())),
);
schema.properties.insert(
"product_id".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(u32::schema())),
);
schema.properties.insert(
"name".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(String::schema())),
);
schema.properties.insert(
"quantity".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(i32::schema())),
);
schema.properties.insert(
"unit_price".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<Currency as ToSchema>::schema(),
)),
);
schema.properties.insert(
"subtotal".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<Currency as ToSchema>::schema(),
)),
);
schema.properties.insert(
"active_till".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<DateTime<Utc> as ToSchema>::schema(),
)),
);
schema
}
}
// Schema generation for Sale
impl ToSchema for Sale {
fn schema() -> poem_openapi::registry::MetaSchema {
let mut schema = poem_openapi::registry::MetaSchema::new("Sale");
schema.properties.insert(
"id".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(u32::schema())),
);
schema.properties.insert(
"company_id".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(u32::schema())),
);
schema.properties.insert(
"buyer_name".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(String::schema())),
);
schema.properties.insert(
"buyer_email".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(String::schema())),
);
schema.properties.insert(
"total_amount".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<Currency as ToSchema>::schema(),
)),
);
schema.properties.insert(
"status".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<SaleStatus as ToSchema>::schema(),
)),
);
schema.properties.insert(
"sale_date".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<DateTime<Utc> as ToSchema>::schema(),
)),
);
schema.properties.insert(
"created_at".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<DateTime<Utc> as ToSchema>::schema(),
)),
);
schema.properties.insert(
"updated_at".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<DateTime<Utc> as ToSchema>::schema(),
)),
);
schema.properties.insert(
"items".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<Vec<SaleItem> as ToSchema>::schema(),
)),
);
schema
}
}

View File

@ -1,92 +0,0 @@
You're a Rust developer assistant.
Please generate a complete Rust web server using `poem` and `poem-openapi`. It should:
1. Create a basic `#[OpenApi]` interface.
2. Automatically generate and serve an OpenAPI 3.0 spec at `/api/openapi.json`.
3. Serve a Swagger UI interface at `/docs`.
4. Expose a function I define in another module (e.g., `my_logic::do_something()`).
5. The endpoint should be `/do` (HTTP GET) and return a JSON response.
Use these crates:
- `poem`
- `poem-openapi`
- `tokio`
- `uuid` (optional, if needed)
We want to create crud and also other methods of herodb/src/zaz/models
To see how we call this model logic see herodb/src/zaz/cmd/examples.rs
And use it inside the API handler.
The project should have:
- `main.rs` (entry point)
- `sale.rs` (logic per file e.g. sale, product, ...)
- Clear type annotations and minimal boilerplate.
```
---
## 🧪 Example Output from AI Based on That Prompt
### `Cargo.toml`
```toml
[package]
name = "poem-api-server"
version = "0.1.0"
edition = "2021"
[dependencies]
poem = "1"
poem-openapi = "3"
tokio = { version = "1", features = ["full"] }
```
### `src/main.rs`
```rust
use poem::{listener::TcpListener, Route, Server};
use poem_openapi::{payload::Json, OpenApi, OpenApiService};
mod my_logic;
struct Api;
#[OpenApi]
impl Api {
#[oai(path = "/do", method = "get")]
async fn do_action(&self) -> Json<String> {
Json(my_logic::do_something())
}
}
#[tokio::main]
async fn main() {
let api_service =
OpenApiService::new(Api, "My API", "1.0").server("http://localhost:3000/api");
let ui = api_service.swagger_ui();
let app = Route::new()
.nest("/api", api_service)
.nest("/docs", ui);
Server::new(TcpListener::bind("127.0.0.1:3000"))
.run(app)
.await
.unwrap();
}
```
---
### ✅ Result
- Open `/api/do` → Calls your logic and returns a JSON response.
- Open `/docs` → Interactive Swagger UI
- Open `/api/openapi.json` → Full OpenAPI spec
---

View File

@ -1,41 +0,0 @@
//! Server module for the HeroDB API
//!
//! This module provides a web API server using Poem and OpenAPI.
pub mod api;
pub mod models;
use poem::{
listener::TcpListener,
Route,
Server
};
use poem_openapi::OpenApiService;
use std::path::PathBuf;
/// Start the API server
pub async fn start_server(db_path: PathBuf, host: &str, port: u16) -> Result<(), std::io::Error> {
// Create the API service
let api_service = OpenApiService::new(
api::Api::new(db_path),
"HeroDB API",
env!("CARGO_PKG_VERSION"),
)
.server(format!("http://{}:{}/api", host, port));
// Create Swagger UI
let swagger_ui = api_service.swagger_ui();
// Create the main route
let app = Route::new()
.nest("/api", api_service)
.nest("/swagger", swagger_ui);
// Start the server
println!("Starting server on {}:{}", host, port);
println!("API Documentation: http://{}:{}/swagger", host, port);
Server::new(TcpListener::bind(format!("{}:{}", host, port)))
.run(app)
.await
}

View File

@ -1,145 +0,0 @@
//! API models for the HeroDB server
use crate::zaz::models::sale::SaleStatus;
use poem_openapi::{Object, ApiResponse};
use serde::{Deserialize, Serialize};
/// API error response
#[derive(Debug, Object)]
pub struct ApiError {
/// Error code
pub code: u16,
/// Error message
pub message: String,
}
impl ApiError {
pub fn not_found(id: impl ToString) -> Self {
Self {
code: 404,
message: format!("Resource with ID {} not found", id.to_string()),
}
}
pub fn internal_error(msg: impl ToString) -> Self {
Self {
code: 500,
message: msg.to_string(),
}
}
}
/// API success response
#[derive(Debug, Object)]
pub struct SuccessResponse {
/// Success flag
pub success: bool,
/// Success message
pub message: String,
}
/// User create request
#[derive(Debug, Object)]
pub struct UserCreate {
/// User name
pub name: String,
/// User email
pub email: String,
/// User password
pub password: String,
/// User company
pub company: String,
/// User role
pub role: String,
}
/// User update request
#[derive(Debug, Object)]
pub struct UserUpdate {
/// User name
#[oai(skip_serializing_if_is_none)]
pub name: Option<String>,
/// User email
#[oai(skip_serializing_if_is_none)]
pub email: Option<String>,
/// User password
#[oai(skip_serializing_if_is_none)]
pub password: Option<String>,
/// User company
#[oai(skip_serializing_if_is_none)]
pub company: Option<String>,
/// User role
#[oai(skip_serializing_if_is_none)]
pub role: Option<String>,
}
/// Sale item create request
#[derive(Debug, Serialize, Deserialize, Object)]
pub struct SaleItemCreate {
/// Product ID
pub product_id: u32,
/// Item name
pub name: String,
/// Quantity
pub quantity: i32,
/// Unit price
pub unit_price: f64,
}
/// Sale create request
#[derive(Debug, Serialize, Deserialize, Object)]
pub struct SaleCreate {
/// Company ID
pub company_id: u32,
/// Buyer name
pub buyer_name: String,
/// Buyer email
pub buyer_email: String,
/// Currency code
pub currency_code: String,
/// Items
#[oai(skip_serializing_if_is_none)]
pub items: Option<Vec<SaleItemCreate>>,
}
/// Sale status update request
#[derive(Debug, Serialize, Deserialize, Object)]
pub struct SaleStatusUpdate {
/// New status
pub status: String,
}
impl SaleStatusUpdate {
pub fn parse_status(&self) -> Result<SaleStatus, ApiError> {
match self.status.to_lowercase().as_str() {
"pending" => Ok(SaleStatus::Pending),
"completed" => Ok(SaleStatus::Completed),
"cancelled" => Ok(SaleStatus::Cancelled),
_ => Err(ApiError {
code: 400,
message: format!("Invalid status: {}", self.status),
})
}
}
}
// Define API responses
#[derive(Debug, ApiResponse)]
pub enum UserResponse {
#[oai(status = 200)]
Ok(poem_openapi::payload::Json<String>),
#[oai(status = 404)]
NotFound(poem_openapi::payload::Json<ApiError>),
#[oai(status = 500)]
InternalError(poem_openapi::payload::Json<ApiError>),
}
#[derive(Debug, ApiResponse)]
pub enum SuccessOrError {
#[oai(status = 200)]
Ok(poem_openapi::payload::Json<SuccessResponse>),
#[oai(status = 404)]
NotFound(poem_openapi::payload::Json<ApiError>),
#[oai(status = 500)]
InternalError(poem_openapi::payload::Json<ApiError>),
}