lxy 0.1.1

A convenient async http and RPC framework in Rust
Documentation
use std::pin::Pin;
use std::sync::Arc;

use crate::App;
use crate::config;
use crate::container::{Container, RawContainer, TypeNotRegistered};
use crate::error::{Result, TypedError};
use crate::server::BoxedServer;
use crate::server::Server;
use crate::tracing;
use crate::tracing::config::TracingConfig;

crate::define_errors! {
    crate::error::ErrorCode::Internal => [
        BootstrapError,
    ]
}

/// Builder for configuring the application.
pub struct AppBuilder {
  app: App,
  config_provider: config::TomlConfigProvider<String>,
  tracing_builder: tracing::TracingBuilder,
  servers: Vec<BoxedServer>,
}

impl Default for AppBuilder {
  fn default() -> Self {
    let app = App::with_container(RawContainer::new());

    let config_provider = config::TomlConfigProvider::new("app.toml").build();

    let mut builder = Self {
      app,
      config_provider,
      tracing_builder: tracing::TracingBuilder::default(),
      servers: vec![],
    };
    builder.init().unwrap();
    builder
  }
}

impl AppBuilder {
  pub fn new() -> Self {
    Self::default()
  }

  pub fn app(&self) -> &App {
    &self.app
  }

  pub fn add_server<S>(&mut self, server: S) -> &mut Self
  where
    S: Server<Future = Pin<Box<dyn Future<Output = ()> + Send>>> + Send + 'static,
  {
    self.servers.push(Box::new(server));
    self
  }

  pub fn compose<F>(&mut self, composer: F) -> &mut Self
  where
    F: Fn(&mut AppBuilder),
  {
    composer(self);
    self
  }

  pub fn bind_config<T>(&mut self) -> Result<&mut Self>
  where
    T: 'static + config::Config + serde::de::DeserializeOwned,
  {
    let config = self.config_provider.parse::<T>()?;
    self.bind(config);
    Ok(self)
  }

  pub fn get_config<T>(&self) -> Result<Arc<T>>
  where
    T: 'static + config::Config,
  {
    self.get::<T>().map_err(|e| {
      if e.is(TypeNotRegistered) {
        config::ConfigNotRegistered::error(format!(
          "Configuration of type {} is not registered",
          std::any::type_name::<T>()
        ))
      } else {
        e
      }
    })
  }

  /// Starts all registered servers concurrently
  pub async fn start(mut self) -> Result<()> {
    use tracing::info;

    self.bootstrap_core()?;

    info!("🚀 Starting application...");

    match self.servers.len() {
      0 => {
        tracing::warn!("No servers registered");
      }
      1 => {
        let server = self.servers.into_iter().next().unwrap();
        server.start().await;
      }
      _ => {
        // For multiple servers, spawn all and wait for all to complete
        let mut set = tokio::task::JoinSet::new();
        for server in self.servers {
          set.spawn(async move { server.start().await });
        }
        // Wait for all servers to complete
        while set.join_next().await.is_some() {}
      }
    }
    Ok(())
  }

  fn init(&mut self) -> Result<()> {
    // Set the temporary tracing logger to capture output during initialization
    self.set_bootstrap_logger();

    // Initialize default configurations or services here
    self.configure_core()?;
    Ok(())
  }

  fn configure_core(&mut self) -> Result<()> {
    self.bind_config::<tracing::config::TracingConfig>()?;
    self.tracing_builder.with_config(true);
    Ok(())
  }

  fn bootstrap_core(&mut self) -> Result<()> {
    #[cfg(feature = "otel")]
    self.bootstrap_telemetry()?;

    self.bootstrap_logger()?;
    Ok(())
  }

  /// Bootstraps OpenTelemetry tracing integration.
  ///
  /// This is called automatically during application bootstrap when the `otel` feature is enabled.
  /// It reads the `[otel]` configuration section and sets up:
  /// - A tracer provider with the configured OTLP exporter
  /// - An OpenTelemetry tracing layer for `tracing-subscriber`
  #[cfg(feature = "otel")]
  fn bootstrap_telemetry(&mut self) -> Result<()> {
    use crate::telemetry::{OtelConfig, create_otel_layer, init_tracer_provider};

    self.bind_config::<OtelConfig>()?;
    let otel_config = self.get_config::<OtelConfig>()?;

    let provider = init_tracer_provider(&otel_config);
    let otel_layer = create_otel_layer(&provider);
    self.tracing_builder.add_layer("otel", otel_layer);
    self.bind(provider);
    tracing::info!("OpenTelemetry tracing enabled");
    Ok(())
  }

  /// Configures the tracing system using the provided configuration function.
  ///
  /// See [`tracing::TracingBuilder`].
  pub fn with_tracing<F>(&mut self, configure: F) -> &mut Self
  where
    F: FnOnce(&mut tracing::TracingBuilder),
  {
    configure(&mut self.tracing_builder);

    self
  }

  /// Deprecated: Use `with_tracing` instead.
  #[deprecated(since = "0.2.0", note = "Use `with_tracing` instead")]
  pub fn with_logger<F>(&mut self, configure: F) -> &mut Self
  where
    F: FnOnce(&mut tracing::TracingBuilder),
  {
    self.with_tracing(configure)
  }

  fn bootstrap_logger(&mut self) -> Result<()> {
    let log_config = self.get_config::<TracingConfig>()?;
    let (subscriber, keeper) = self.tracing_builder.build(log_config.as_ref());

    self.drop_bootstrap_logger();
    if cfg!(not(test)) {
      ::tracing::subscriber::set_global_default(subscriber).map_err(|e| {
        BootstrapError::error(format!("Failed to set global tracing subscriber: {}", e))
      })?;
    }

    self.bind(keeper);
    Ok(())
  }

  fn set_bootstrap_logger(&mut self) {
    self.bind(tracing::boot::BootstrapperSubscriber::new());
  }

  fn drop_bootstrap_logger(&mut self) {
    self
      .app
      .container
      .drop::<tracing::boot::BootstrapperSubscriber>();
  }
}

impl Container for AppBuilder {
  fn add<F, T, Deps>(&mut self, factory: F)
  where
    F: crate::container::Factory<T, Deps>,
    T: 'static,
    Deps: 'static,
  {
    self.app.container.add(factory);
  }

  fn bind<T>(&mut self, instance: T)
  where
    T: 'static,
  {
    self.app.container.bind(instance);
  }

  fn get<T>(&self) -> Result<Arc<T>>
  where
    T: 'static,
  {
    self.app.container.get::<T>()
  }

  fn drop<T>(&mut self)
  where
    T: 'static,
  {
    self.app.container.drop::<T>();
  }
}