diff --git a/herodb/Cargo.lock b/herodb/Cargo.lock index 38b7580..d45ba75 100644 --- a/herodb/Cargo.lock +++ b/herodb/Cargo.lock @@ -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" diff --git a/herodb/Cargo.toml b/herodb/Cargo.toml index e8c543c..b503016 100644 --- a/herodb/Cargo.toml +++ b/herodb/Cargo.toml @@ -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" diff --git a/herodb/README.md b/herodb/README.md new file mode 100644 index 0000000..4ac2b11 --- /dev/null +++ b/herodb/README.md @@ -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::() + .register_model::() + .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::(&"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 \ No newline at end of file diff --git a/herodb/src/db/macros.rs b/herodb/src/db/macros.rs new file mode 100644 index 0000000..e77a289 --- /dev/null +++ b/herodb/src/db/macros.rs @@ -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 [](&self, item: &$model) -> SledDBResult<()> { + self.set(item) + } + + /// Get a model instance by its ID + pub fn [](&self, id: u32) -> SledDBResult<$model> { + self.get::<$model>(&id.to_string()) + } + + /// Delete a model instance by its ID + pub fn [](&self, id: u32) -> SledDBResult<()> { + self.delete::<$model>(&id.to_string()) + } + + /// List all model instances + pub fn [](&self) -> SledDBResult> { + self.list::<$model>() + } + } + } + }; +} \ No newline at end of file diff --git a/herodb/src/db/mod.rs b/herodb/src/db/mod.rs index 2b17435..fd73afa 100644 --- a/herodb/src/db/mod.rs +++ b/herodb/src/db/mod.rs @@ -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}; diff --git a/herodb/src/db/model_methods.rs b/herodb/src/db/model_methods.rs new file mode 100644 index 0000000..415a610 --- /dev/null +++ b/herodb/src/db/model_methods.rs @@ -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); \ No newline at end of file diff --git a/herodb/src/lib.rs b/herodb/src/lib.rs index f200d15..3b68f1e 100644 --- a/herodb/src/lib.rs +++ b/herodb/src/lib.rs @@ -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; diff --git a/herodb/src/models/biz/README.md b/herodb/src/models/biz/README.md index f0b2f78..4aa684c 100644 --- a/herodb/src/models/biz/README.md +++ b/herodb/src/models/biz/README.md @@ -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. \ No newline at end of file diff --git a/herodb/src/server/api.rs b/herodb/src/server/api.rs deleted file mode 100644 index 40c09e7..0000000 --- a/herodb/src/server/api.rs +++ /dev/null @@ -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>, -} - -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 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 { - let db = self.db.lock().unwrap(); - match db.list::() { - 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) -> UserResponse { - let db = self.db.lock().unwrap(); - match db.get::(&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, - ) -> UserResponse { - let db = self.db.lock().unwrap(); - - // Find the next available ID - let users: Vec = 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, - user: Json, - ) -> 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) -> SuccessOrError { - let db = self.db.lock().unwrap(); - - match db.delete::(&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 { - let db = self.db.lock().unwrap(); - match db.list::() { - 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) -> UserResponse { - let db = self.db.lock().unwrap(); - match db.get::(&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 { - let db = self.db.lock().unwrap(); - match db.list::() { - 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) -> UserResponse { - let db = self.db.lock().unwrap(); - match db.get::(&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, - ) -> UserResponse { - let db = self.db.lock().unwrap(); - - // Find the next available ID - let sales: Vec = 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, - status: Json, - ) -> 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) -> SuccessOrError { - let db = self.db.lock().unwrap(); - - match db.delete::(&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))) - } - } - } -} diff --git a/herodb/src/server/extensions.rs b/herodb/src/server/extensions.rs deleted file mode 100644 index 1af319f..0000000 --- a/herodb/src/server/extensions.rs +++ /dev/null @@ -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 compatible with OpenAPI -impl Type for DateTime { - 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 { - Some(self.to_rfc3339()) - } - - fn raw_element_iter<'a>( - &'a self, - ) -> Box + '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 { - Some(serde_json::json!({ - "amount": self.amount, - "currency_code": self.currency_code - })) - } - - fn raw_element_iter<'a>( - &'a self, - ) -> Box + '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 { - 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 + '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( - as ToSchema>::schema(), - )), - ); - schema.properties.insert( - "updated_at".to_string(), - poem_openapi::registry::MetaSchemaRef::Inline(Box::new( - 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( - ::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( - as ToSchema>::schema(), - )), - ); - schema.properties.insert( - "updated_at".to_string(), - poem_openapi::registry::MetaSchemaRef::Inline(Box::new( - 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( - ::schema(), - )), - ); - schema.properties.insert( - "subtotal".to_string(), - poem_openapi::registry::MetaSchemaRef::Inline(Box::new( - ::schema(), - )), - ); - schema.properties.insert( - "active_till".to_string(), - poem_openapi::registry::MetaSchemaRef::Inline(Box::new( - 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( - ::schema(), - )), - ); - schema.properties.insert( - "status".to_string(), - poem_openapi::registry::MetaSchemaRef::Inline(Box::new( - ::schema(), - )), - ); - schema.properties.insert( - "sale_date".to_string(), - poem_openapi::registry::MetaSchemaRef::Inline(Box::new( - as ToSchema>::schema(), - )), - ); - schema.properties.insert( - "created_at".to_string(), - poem_openapi::registry::MetaSchemaRef::Inline(Box::new( - as ToSchema>::schema(), - )), - ); - schema.properties.insert( - "updated_at".to_string(), - poem_openapi::registry::MetaSchemaRef::Inline(Box::new( - as ToSchema>::schema(), - )), - ); - schema.properties.insert( - "items".to_string(), - poem_openapi::registry::MetaSchemaRef::Inline(Box::new( - as ToSchema>::schema(), - )), - ); - schema - } -} \ No newline at end of file diff --git a/herodb/src/server/instructions.md b/herodb/src/server/instructions.md deleted file mode 100644 index 98d610b..0000000 --- a/herodb/src/server/instructions.md +++ /dev/null @@ -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 { - 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 - ---- diff --git a/herodb/src/server/mod.rs b/herodb/src/server/mod.rs deleted file mode 100644 index ecc3db2..0000000 --- a/herodb/src/server/mod.rs +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/herodb/src/server/models.rs b/herodb/src/server/models.rs deleted file mode 100644 index b368458..0000000 --- a/herodb/src/server/models.rs +++ /dev/null @@ -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, - /// User email - #[oai(skip_serializing_if_is_none)] - pub email: Option, - /// User password - #[oai(skip_serializing_if_is_none)] - pub password: Option, - /// User company - #[oai(skip_serializing_if_is_none)] - pub company: Option, - /// User role - #[oai(skip_serializing_if_is_none)] - pub role: Option, -} - -/// 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>, -} - -/// 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 { - 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), - #[oai(status = 404)] - NotFound(poem_openapi::payload::Json), - #[oai(status = 500)] - InternalError(poem_openapi::payload::Json), -} - -#[derive(Debug, ApiResponse)] -pub enum SuccessOrError { - #[oai(status = 200)] - Ok(poem_openapi::payload::Json), - #[oai(status = 404)] - NotFound(poem_openapi::payload::Json), - #[oai(status = 500)] - InternalError(poem_openapi::payload::Json), -} \ No newline at end of file