at-jet 0.7.2

High-performance HTTP + Protobuf API framework for mobile services
Documentation
//! Basic AT-Jet Server Example
//!
//! Demonstrates how to create a simple HTTP + Protobuf API server with dual format support.
//!
//! Run with: cargo run --example basic_server
//!
//! Features demonstrated:
//! - Protobuf API endpoints (production format)
//! - JSON debug format (requires X-Debug-Format header)
//! - Request/response handling with ApiRequest/ApiResponse
//! - Smart tracing (health/metrics endpoints at TRACE level)
//! - Tracing initialization via init_tracing()
//! - Graceful shutdown via serve_with_shutdown()
//! - Startup banner via print_banner()
//!
//! For Prometheus metrics, enable the `metrics` feature:
//!   cargo run --example basic_server --features metrics
//!
//! For JWT authentication, enable the `jwt` feature:
//!   cargo run --example basic_server --features jwt
//!
//! For session management, enable the `session` feature:
//!   cargo run --example basic_server --features session

use {at_jet::{dual_format::configure_debug_keys,
              prelude::*},
     axum::http::StatusCode,
     serde::{Deserialize,
             Serialize}};

// Include generated protobuf code
pub mod proto {
  include!(concat!(env!("OUT_DIR"), "/at_jet.example.rs"));
}

use proto::{CreateUserRequest,
            DeleteUserResponse,
            ListUsersResponse,
            User};

// Implement Serialize/Deserialize for proto types to enable JSON format
impl Serialize for User {
  fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
  where
    S: serde::Serializer, {
    use serde::ser::SerializeStruct;
    let mut state = serializer.serialize_struct("User", 4)?;
    state.serialize_field("id", &self.id)?;
    state.serialize_field("name", &self.name)?;
    state.serialize_field("email", &self.email)?;
    state.serialize_field("created_at", &self.created_at)?;
    state.end()
  }
}

impl<'de> Deserialize<'de> for CreateUserRequest {
  fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
  where
    D: serde::Deserializer<'de>, {
    #[derive(Deserialize)]
    struct Helper {
      name:  String,
      email: String,
    }
    let helper = Helper::deserialize(deserializer)?;
    Ok(CreateUserRequest {
      name:  helper.name,
      email: helper.email,
    })
  }
}

impl Serialize for ListUsersResponse {
  fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
  where
    S: serde::Serializer, {
    use serde::ser::SerializeStruct;
    let mut state = serializer.serialize_struct("ListUsersResponse", 4)?;
    state.serialize_field("users", &self.users)?;
    state.serialize_field("total", &self.total)?;
    state.serialize_field("page", &self.page)?;
    state.serialize_field("page_size", &self.page_size)?;
    state.end()
  }
}

impl Serialize for DeleteUserResponse {
  fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
  where
    S: serde::Serializer, {
    use serde::ser::SerializeStruct;
    let mut state = serializer.serialize_struct("DeleteUserResponse", 2)?;
    state.serialize_field("success", &self.success)?;
    state.serialize_field("message", &self.message)?;
    state.end()
  }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
  // Print startup banner before tracing is initialized
  JetServer::print_banner("basic-server", env!("CARGO_PKG_VERSION"), &[("Mode", "example")]);

  // Initialize tracing using at-jet's unified tracing setup
  let _tracing_guard = init_tracing(&TracingConfig {
    level: "debug".to_string(),
    env_filter_conf: vec!["at_jet=debug".to_string()],
    ..Default::default()
  });

  // Configure debug keys for JSON format
  // In production, use secure keys and distribute to authorized developers
  configure_debug_keys(vec!["dev-debug-key".to_string(), "qa-debug-key".to_string()]);

  // Build server
  let server = JetServer::new()
    .route("/health", get(health_check))
    .route("/api/users", get(list_users).post(create_user))
    .route("/api/users/:id", get(get_user).delete(delete_user))
    .with_cors()
    .with_compression()
    .with_tracing();

  tracing::info!("AT-Jet example server starting on http://0.0.0.0:8080");
  tracing::info!("Endpoints: GET /health, GET/POST /api/users, GET/DELETE /api/users/:id");
  tracing::info!("Protobuf (default) | JSON (debug): X-Debug-Format: dev-debug-key");

  // Use serve_with_shutdown for graceful Ctrl+C handling
  server.serve_with_shutdown("0.0.0.0:8080").await?;

  Ok(())
}

// Health check endpoint (plain text)
async fn health_check() -> &'static str {
  "OK"
}

// List users endpoint - supports both Protobuf and JSON
async fn list_users(AcceptFormat(format): AcceptFormat) -> ApiResponse<ListUsersResponse> {
  let users = vec![
    User {
      id:         1,
      name:       "Alice".to_string(),
      email:      "alice@example.com".to_string(),
      created_at: 1704067200,
    },
    User {
      id:         2,
      name:       "Bob".to_string(),
      email:      "bob@example.com".to_string(),
      created_at: 1704153600,
    },
  ];

  let response = ListUsersResponse {
    users,
    total: 2,
    page: 1,
    page_size: 10,
  };

  ApiResponse::ok(format, response)
}

// Get user by ID - supports both Protobuf and JSON
async fn get_user(
  AcceptFormat(format): AcceptFormat,
  Path(id): Path<i32>,
) -> std::result::Result<ApiResponse<User>, StatusCode> {
  // Simulate database lookup
  if id == 1 {
    let user = User {
      id:         1,
      name:       "Alice".to_string(),
      email:      "alice@example.com".to_string(),
      created_at: 1704067200,
    };
    Ok(ApiResponse::ok(format, user))
  } else if id == 2 {
    let user = User {
      id:         2,
      name:       "Bob".to_string(),
      email:      "bob@example.com".to_string(),
      created_at: 1704153600,
    };
    Ok(ApiResponse::ok(format, user))
  } else {
    Err(StatusCode::NOT_FOUND)
  }
}

// Create user endpoint - supports both Protobuf and JSON input/output
async fn create_user(request: ApiRequest<CreateUserRequest>) -> std::result::Result<ApiResponse<User>, StatusCode> {
  // Create user (in real app, save to database)
  let user = User {
    id:         100, // In real app, generate ID
    name:       request.body.name.clone(),
    email:      request.body.email.clone(),
    created_at: std::time::SystemTime::now()
      .duration_since(std::time::UNIX_EPOCH)
      .map(|d| d.as_secs() as i64)
      .unwrap_or(0),
  };

  Ok(request.created(user))
}

// Delete user endpoint - supports both Protobuf and JSON
async fn delete_user(
  AcceptFormat(format): AcceptFormat,
  Path(id): Path<i32>,
) -> std::result::Result<ApiResponse<DeleteUserResponse>, StatusCode> {
  // Simulate deletion
  if id > 0 && id <= 100 {
    let response = DeleteUserResponse {
      success: true,
      message: format!("User {} deleted successfully", id),
    };
    Ok(ApiResponse::ok(format, response))
  } else {
    Err(StatusCode::NOT_FOUND)
  }
}