...
This commit is contained in:
parent
8a5e41a265
commit
186e339740
7
herodb/Cargo.lock
generated
7
herodb/Cargo.lock
generated
@ -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"
|
||||
|
@ -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
81
herodb/README.md
Normal 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
29
herodb/src/db/macros.rs
Normal 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>()
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@ -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};
|
||||
|
15
herodb/src/db/model_methods.rs
Normal file
15
herodb/src/db/model_methods.rs
Normal 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);
|
@ -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;
|
||||
|
@ -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.
|
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
---
|
@ -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
|
||||
}
|
@ -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>),
|
||||
}
|
Loading…
Reference in New Issue
Block a user