diff --git a/examples/custom_types/Cargo.toml b/examples/custom_types/Cargo.toml new file mode 100644 index 0000000..d54861a --- /dev/null +++ b/examples/custom_types/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "custom_types" +version = "0.1.0" +edition = "2021" + +[dependencies] +gotcha = { path = "../../gotcha" } +tokio = { version = "1", features = ["full"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tracing-subscriber = "0.3" \ No newline at end of file diff --git a/examples/custom_types/src/main.rs b/examples/custom_types/src/main.rs new file mode 100644 index 0000000..6f16607 --- /dev/null +++ b/examples/custom_types/src/main.rs @@ -0,0 +1,58 @@ +use gotcha::prelude::*; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; + +// Custom application state +#[derive(Clone, Default)] +struct AppState { + request_counter: Arc, +} + +// Custom application configuration +#[derive(Clone, Default, Serialize, Deserialize)] +struct AppConfig { + api_key: String, + max_connections: u32, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + tracing_subscriber::fmt::init(); + + println!("🚀 Demonstrating new Gotcha API improvements"); + println!(); + + // The NEW way - clean and simple! + println!("✨ NEW API - No more <(), ()> needed!"); + println!(" Gotcha::with_types::()"); + println!(" Gotcha::with_state::()"); + println!(" Gotcha::with_config::()"); + + // Example 1: Using with_types for both state and config + let _app1 = Gotcha::with_types::() + .state(AppState::default()) + .get("/", || async { "App with custom state and config" }); + + // Example 2: Using with_state for custom state only + let _app2 = Gotcha::with_state::() + .state(AppState::default()) + .get("/", || async { "App with custom state" }); + + // Example 3: Using with_config for custom config only + let _app3 = Gotcha::with_config::() + .with_env_config("APP") + .get("/", || async { "App with custom config" }); + + // Example 4: Traditional approach for simple apps still works + let _app4 = Gotcha::new() + .get("/", || async { "Simple app" }); + + println!(); + println!("✅ All API variations compile successfully!"); + println!(); + println!("💡 The old way would have required:"); + println!(" Gotcha::<(), ()>::with_types::() // 😕 Awkward!"); + + Ok(()) +} \ No newline at end of file diff --git a/examples/custom_types_real/Cargo.toml b/examples/custom_types_real/Cargo.toml new file mode 100644 index 0000000..13c2e27 --- /dev/null +++ b/examples/custom_types_real/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "custom_types_real" +version = "0.1.0" +edition = "2021" + +[dependencies] +gotcha = { path = "../../gotcha" } +tokio = { version = "1", features = ["full"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tracing-subscriber = "0.3" \ No newline at end of file diff --git a/examples/custom_types_real/src/main.rs b/examples/custom_types_real/src/main.rs new file mode 100644 index 0000000..05e13f5 --- /dev/null +++ b/examples/custom_types_real/src/main.rs @@ -0,0 +1,142 @@ +//! Real-world example showing how the improved Gotcha API makes it easier +//! to work with custom state and configuration types. + +use gotcha::prelude::*; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; +use tokio::sync::RwLock; +use std::collections::HashMap; + +// Application state with a counter and a simple in-memory store +#[derive(Clone, Default)] +struct AppState { + request_counter: Arc, + users: Arc>>, +} + +// Application configuration +#[derive(Clone, Serialize, Deserialize)] +struct AppConfig { + api_key: String, + max_users: usize, +} + +impl Default for AppConfig { + fn default() -> Self { + Self { + api_key: "demo-key".to_string(), + max_users: 100, + } + } +} + +// Domain model +#[derive(Clone, Serialize, Deserialize, Debug)] +struct User { + id: u64, + name: String, + email: String, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + tracing_subscriber::fmt::init(); + + println!("🚀 Starting Real-World Gotcha Example with Custom Types"); + println!(); + + // Initialize state with some sample data + let mut initial_users = HashMap::new(); + initial_users.insert(1, User { + id: 1, + name: "Alice".to_string(), + email: "alice@example.com".to_string(), + }); + initial_users.insert(2, User { + id: 2, + name: "Bob".to_string(), + email: "bob@example.com".to_string(), + }); + + let state = AppState { + request_counter: Arc::new(AtomicU64::new(0)), + users: Arc::new(RwLock::new(initial_users)), + }; + + // ✨ The NEW clean API - No more Gotcha::<(), ()>::with_types! + let app = Gotcha::with_types::() + .state(state) + .with_env_config("APP") + + // Home route with request counter + .get("/", |ctx: State>| async move { + let count = ctx.state.request_counter.fetch_add(1, Ordering::SeqCst); + format!("Welcome to Gotcha! This is request #{}", count + 1) + }) + + // Get all users + .get("/users", |ctx: State>| async move { + let users = ctx.state.users.read().await; + let users_vec: Vec<&User> = users.values().collect(); + Json(users_vec) + }) + + // Get user by ID + .get("/users/:id", | + Path(id): Path, + ctx: State> + | async move { + let users = ctx.state.users.read().await; + match users.get(&id) { + Some(user) => Ok(Json(user.clone())), + None => Err((StatusCode::NOT_FOUND, "User not found")) + } + }) + + // Create new user + .post("/users", || async { + Json(serde_json::json!({ + "message": "User creation endpoint (simplified for demo)" + })) + }) + + // Show configuration + .get("/config", |ctx: State>| async move { + Json(serde_json::json!({ + "api_key": if ctx.config.application.api_key.is_empty() { + "not-configured" + } else { + "***hidden***" + }, + "max_users": ctx.config.application.max_users + })) + }) + + // Health check + .get("/health", || async { + Json(serde_json::json!({ + "status": "healthy", + "service": "gotcha-example" + })) + }); + + println!("✅ API endpoints:"); + println!(" GET / - Home with request counter"); + println!(" GET /users - List all users"); + println!(" GET /users/:id - Get user by ID"); + println!(" POST /users - Create new user"); + println!(" GET /config - Show configuration"); + println!(" GET /health - Health check"); + println!(); + println!("🌐 Server running at http://127.0.0.1:3000"); + println!(); + println!("💡 Try:"); + println!(" curl http://127.0.0.1:3000/users"); + println!(" curl -X POST http://127.0.0.1:3000/users -H 'Content-Type: application/json' -d '{{\"name\":\"Charlie\",\"email\":\"charlie@example.com\"}}'"); + println!(); + + app.listen("127.0.0.1:3000").await?; + + Ok(()) +} \ No newline at end of file diff --git a/examples/custom_types_simple/Cargo.toml b/examples/custom_types_simple/Cargo.toml new file mode 100644 index 0000000..34d275f --- /dev/null +++ b/examples/custom_types_simple/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "custom_types_simple" +version = "0.1.0" +edition = "2021" + +[dependencies] +gotcha = { path = "../../gotcha" } +tokio = { version = "1", features = ["full"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tracing-subscriber = "0.3" \ No newline at end of file diff --git a/examples/custom_types_simple/src/main.rs b/examples/custom_types_simple/src/main.rs new file mode 100644 index 0000000..5032a72 --- /dev/null +++ b/examples/custom_types_simple/src/main.rs @@ -0,0 +1,70 @@ +//! Simplified example showcasing the new Gotcha API improvements + +use gotcha::prelude::*; +use serde::{Deserialize, Serialize}; + +// Custom application state +#[derive(Clone, Default)] +struct MyState { + name: String, +} + +// Custom application configuration +#[derive(Clone, Default, Serialize, Deserialize)] +struct MyConfig { + api_key: String, + port: u16, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + tracing_subscriber::fmt::init(); + + println!("🚀 Gotcha API Improvements Demo"); + println!("================================"); + println!(); + + // ✨ The OLD way (before the improvement) would have required: + // Gotcha::<(), ()>::with_types::() // 😕 Awkward! + + // ✨ The NEW way - clean and intuitive: + println!("✅ NEW API - Direct and clean:"); + println!(); + println!(" // For custom state and config:"); + println!(" Gotcha::with_types::()"); + println!(); + println!(" // For custom state only:"); + println!(" Gotcha::with_state::()"); + println!(); + println!(" // For custom config only:"); + println!(" Gotcha::with_config::()"); + println!(); + println!(" // For simple apps (unchanged):"); + println!(" Gotcha::new()"); + println!(); + + // Test that all variations compile correctly + let _app1 = Gotcha::with_types::() + .state(MyState { name: "Example".to_string() }) + .get("/", || async { "Custom state and config" }); + + let _app2 = Gotcha::with_state::() + .state(MyState { name: "Example".to_string() }) + .get("/", || async { "Custom state only" }); + + let _app3 = Gotcha::with_config::() + .get("/", || async { "Custom config only" }); + + let _app4 = Gotcha::new() + .get("/", || async { "Simple app" }); + + println!("✨ Benefits of the new API:"); + println!(" • No more confusing <(), ()> type parameters"); + println!(" • Cleaner, more intuitive syntax"); + println!(" • Better developer experience"); + println!(" • Same powerful functionality"); + println!(); + println!("✅ All API variations compile successfully!"); + + Ok(()) +} \ No newline at end of file diff --git a/gotcha/src/builder.rs b/gotcha/src/builder.rs index 997975f..69cdaaf 100644 --- a/gotcha/src/builder.rs +++ b/gotcha/src/builder.rs @@ -69,7 +69,7 @@ impl Gotcha { /// # Example /// ```no_run /// use gotcha::prelude::*; - /// + /// /// let app = Gotcha::new() /// .get("/", || async { "Hello World" }); /// ``` @@ -85,16 +85,36 @@ impl Gotcha { } } -impl Gotcha -where - S: Clone + Send + Sync + 'static + Default, - C: Clone + Send + Sync + 'static + Serialize + for<'de> Deserialize<'de> + Default, -{ +impl Gotcha { /// Create a new Gotcha builder with custom state and config types - pub fn with_types() -> Gotcha + /// + /// This is a convenience method that allows you to specify custom types + /// without needing to provide dummy type parameters. + /// + /// # Example + /// ```no_run + /// use gotcha::prelude::*; + /// use serde::{Deserialize, Serialize}; + /// + /// #[derive(Clone, Default)] + /// struct AppState { + /// // your state fields + /// } + /// + /// #[derive(Clone, Default, Serialize, Deserialize)] + /// struct AppConfig { + /// // your config fields + /// } + /// + /// let app = Gotcha::with_types::() + /// .get("/", |state: State| async move { + /// "Hello with custom state" + /// }); + /// ``` + pub fn with_types() -> Gotcha where - NS: Clone + Send + Sync + 'static + Default, - NC: Clone + Send + Sync + 'static + Serialize + for<'de> Deserialize<'de> + Default, + S: Clone + Send + Sync + 'static + Default, + C: Clone + Send + Sync + 'static + Serialize + for<'de> Deserialize<'de> + Default, { Gotcha { router: GotchaRouter::default(), @@ -106,6 +126,77 @@ where } } + /// Create a Gotcha builder with custom state type and default config + /// + /// # Example + /// ```no_run + /// use gotcha::prelude::*; + /// + /// #[derive(Clone, Default)] + /// struct AppState { + /// counter: std::sync::Arc, + /// } + /// + /// let app = Gotcha::with_state::() + /// .state(AppState::default()) + /// .get("/", |state: State| async move { + /// "Hello with custom state" + /// }); + /// ``` + pub fn with_state() -> Gotcha + where + S: Clone + Send + Sync + 'static + Default, + { + Gotcha { + router: GotchaRouter::default(), + host: "127.0.0.1".to_string(), + port: 3000, + state: None, + config: None, + config_builder: None, + } + } + + /// Create a Gotcha builder with custom config type and default state + /// + /// # Example + /// ```no_run + /// use gotcha::prelude::*; + /// use serde::{Deserialize, Serialize}; + /// + /// #[derive(Clone, Default, Serialize, Deserialize)] + /// struct AppConfig { + /// api_key: String, + /// max_connections: u32, + /// } + /// + /// let app = Gotcha::with_config::() + /// .with_env_config("APP") + /// .get("/", |config: State>| async move { + /// "Hello with custom config" + /// }); + /// ``` + pub fn with_config() -> Gotcha + where + C: Clone + Send + Sync + 'static + Serialize + for<'de> Deserialize<'de> + Default, + { + Gotcha { + router: GotchaRouter::default(), + host: "127.0.0.1".to_string(), + port: 3000, + state: None, + config: None, + config_builder: None, + } + } +} + +impl Gotcha +where + S: Clone + Send + Sync + 'static + Default, + C: Clone + Send + Sync + 'static + Serialize + for<'de> Deserialize<'de> + Default, +{ + /// Set the application state pub fn state(mut self, state: S) -> Self { self.state = Some(state); diff --git a/gotcha/tests/builder_api_test.rs b/gotcha/tests/builder_api_test.rs new file mode 100644 index 0000000..0b7879d --- /dev/null +++ b/gotcha/tests/builder_api_test.rs @@ -0,0 +1,57 @@ +//! Tests for the improved Gotcha builder API + +use gotcha::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Default)] +struct TestState { + counter: u32, +} + +#[derive(Clone, Default, Serialize, Deserialize)] +struct TestConfig { + setting: String, +} + +#[test] +fn test_new_api_with_types() { + // Should compile without needing <(), ()> + let _app = Gotcha::with_types::() + .state(TestState::default()) + .get("/", || async { "test" }); +} + +#[test] +fn test_new_api_with_state() { + // Should compile with just state type + let _app = Gotcha::with_state::() + .state(TestState::default()) + .get("/", || async { "test" }); +} + +#[test] +fn test_new_api_with_config() { + // Should compile with just config type + let _app = Gotcha::with_config::() + .get("/", || async { "test" }); +} + +#[test] +fn test_traditional_api_still_works() { + // Traditional API should still work + let _app = Gotcha::new() + .get("/", || async { "test" }); +} + +#[test] +fn test_chaining_works() { + // Test that method chaining works correctly + let _app = Gotcha::with_types::() + .state(TestState::default()) + .host("0.0.0.0") + .port(8080) + .get("/", || async { "home" }) + .post("/users", || async { "create user" }) + .put("/users/:id", || async { "update user" }) + .delete("/users/:id", || async { "delete user" }); +} \ No newline at end of file