holger-server-lib 0.4.0

Holger server library: config, wiring, gRPC service, Rust API
// holger-server-lib/src/lib.rs
//
// Two modes of operation:
//   1. Rust function bindings — use Holger struct directly as a library
//   2. gRPC server — configure exposed_endpoints in RON, call holger.start()

use serde::{Deserialize, Serialize};

use std::{
    fs::File,
    io::BufReader,
    path::Path,
};

use anyhow::Result;
use ron::de::from_reader;

use std::collections::HashMap;
use std::sync::Arc;
use holger_traits::RepositoryBackendTrait;
use crate::exposed::ExposedEndpoint;
use crate::exposed::fast_routes::FastRoutes;
pub use crate::repository::Repository;
pub use crate::storage::StorageEndpoint;

pub mod auth;
pub mod exposed;
pub mod grpc;
pub mod repository;
pub mod storage;

// ========================= WIRING ENGINE =========================

pub fn wire_holger(holger: &mut Holger) -> Result<()> {
    let mut repo_map = HashMap::new();
    let mut exposed_map = HashMap::new();
    let mut storage_map = HashMap::new();

    for repo in &*holger.repositories {
        repo_map.insert(repo.ron_name.clone(), repo as *const Repository);
    }
    for exp in &*holger.exposed_endpoints {
        exposed_map.insert(exp.ron_name.clone(), exp as *const ExposedEndpoint);
    }
    for st in &*holger.storage_endpoints {
        storage_map.insert(st.ron_name.clone(), st as *const StorageEndpoint);
    }

    // Wire repository IO references
    for repo in &mut holger.repositories {
        for name in &repo.ron_upstreams {
            if let Some(ptr) = repo_map.get(name) {
                repo.wired_upstreams.push(*ptr);
            } else {
                return Err(anyhow::anyhow!("Missing upstream repo: {}", name));
            }
        }

        if let Some(io) = &mut repo.ron_in {
            io.wired_storage = *storage_map
                .get(&io.ron_storage_endpoint)
                .ok_or_else(|| anyhow::anyhow!("Missing storage endpoint: {}", io.ron_storage_endpoint))?;
            io.wired_exposed = *exposed_map
                .get(&io.ron_exposed_endpoint)
                .ok_or_else(|| anyhow::anyhow!("Missing exposed endpoint: {}", io.ron_exposed_endpoint))?;
        }

        if let Some(io) = &mut repo.ron_out {
            io.wired_storage = *storage_map
                .get(&io.ron_storage_endpoint)
                .ok_or_else(|| anyhow::anyhow!("Missing storage endpoint: {}", io.ron_storage_endpoint))?;
            io.wired_exposed = *exposed_map
                .get(&io.ron_exposed_endpoint)
                .ok_or_else(|| anyhow::anyhow!("Missing exposed endpoint: {}", io.ron_exposed_endpoint))?;
        }
    }

    // Wire reverse links (endpoint → repos)
    for exp in &mut holger.exposed_endpoints {
        for repo in &holger.repositories {
            if let Some(io) = &repo.ron_out {
                if io.ron_exposed_endpoint == exp.ron_name {
                    exp.wired_out_repositories.push(repo as *const Repository);
                }
            }
        }
    }

    for st in &mut holger.storage_endpoints {
        for repo in &holger.repositories {
            if let Some(io) = &repo.ron_in {
                if io.ron_storage_endpoint == st.ron_name {
                    st.wired_in_repositories.push(repo as *const Repository);
                }
            }
            if let Some(io) = &repo.ron_out {
                if io.ron_storage_endpoint == st.ron_name {
                    st.wired_out_repositories.push(repo as *const Repository);
                }
            }
        }
    }

    // Build route tables for each exposed endpoint
    for exp in &mut holger.exposed_endpoints {
        let mut routes: Vec<(String, Arc<dyn RepositoryBackendTrait>)> = Vec::new();

        for &repo_ptr in &exp.wired_out_repositories {
            if repo_ptr.is_null() { continue; }
            let repo: &Repository = unsafe { &*repo_ptr };
            if let Some(backend_arc) = &repo.backend_repository {
                routes.push((repo.ron_name.clone(), backend_arc.clone()));
            }
        }

        exp.aggregated_routes = if routes.is_empty() {
            None
        } else {
            Some(FastRoutes::new(routes))
        };
    }

    Ok(())
}

// ========================= ROOT HOLGER =========================

#[derive(Serialize, Deserialize)]
pub struct Holger {
    pub repositories: Vec<Repository>,
    pub exposed_endpoints: Vec<ExposedEndpoint>,
    pub storage_endpoints: Vec<StorageEndpoint>,
}

// ========================= LOAD RON CONFIG =========================

pub fn read_ron_config<P: AsRef<Path>>(path: P) -> Result<Holger> {
    let file = File::open(path)?;
    let reader = BufReader::new(file);
    let holger: Holger = from_reader(reader)?;
    Ok(holger)
}

impl Holger {
    // ─── Rust API (direct function bindings) ─────────────────────────

    /// Fetch an artifact by ID from a named repository
    pub fn fetch(&self, repository: &str, id: &holger_traits::ArtifactId) -> Result<Option<Vec<u8>>> {
        let repo = self.get_repo(repository)?;
        repo.fetch(id).map_err(Into::into)
    }

    /// Store an artifact (write-enabled repos only)
    pub fn put(&self, repository: &str, id: &holger_traits::ArtifactId, data: &[u8]) -> Result<()> {
        let repo = self.get_repo(repository)?;
        if !repo.is_writable() {
            anyhow::bail!("Repository '{}' is read-only", repository);
        }
        repo.put(id, data).map_err(Into::into)
    }

    /// List all configured repository names
    pub fn list_repositories(&self) -> Vec<&str> {
        self.repositories.iter().map(|r| r.ron_name.as_str()).collect()
    }

    /// Get a repository backend by name
    fn get_repo(&self, name: &str) -> Result<&Arc<dyn RepositoryBackendTrait>> {
        for repo in &self.repositories {
            if repo.ron_name == name {
                return repo.backend_repository.as_ref()
                    .ok_or_else(|| anyhow::anyhow!("Repository '{}' has no backend", name));
            }
        }
        anyhow::bail!("Repository '{}' not found", name)
    }

    // ─── gRPC server (when configured) ──────────────────────────────

    /// Start gRPC servers for all configured exposed_endpoints
    pub fn start(&self) -> Result<()> {
        for ep in &self.exposed_endpoints {
            if let Some(routes) = &ep.aggregated_routes {
                self.start_grpc_endpoint(&ep.ron_url, routes.clone())?;
            }
        }
        Ok(())
    }

    fn start_grpc_endpoint(&self, addr: &str, routes: FastRoutes) -> Result<()> {
        use crate::grpc::holger_proto::{
            repository_service_server::RepositoryServiceServer,
            archive_service_server::ArchiveServiceServer,
            admin_service_server::AdminServiceServer,
        };
        use crate::grpc::HolgerGrpc;

        let grpc_state = Arc::new(HolgerGrpc::new(routes));
        let listen_addr: std::net::SocketAddr = addr.parse()
            .map_err(|e| anyhow::anyhow!("Invalid gRPC address '{}': {}", addr, e))?;

        tokio::spawn(async move {
            println!("gRPC server listening on {}", listen_addr);
            if let Err(e) = tonic::transport::Server::builder()
                .add_service(RepositoryServiceServer::new(grpc_state.clone()))
                .add_service(ArchiveServiceServer::new(grpc_state.clone()))
                .add_service(AdminServiceServer::new(grpc_state.clone()))
                .serve(listen_addr)
                .await
            {
                eprintln!("gRPC server error: {}", e);
            }
        });

        Ok(())
    }

    pub fn instantiate_backends(&mut self) -> Result<()> {
        for se in &mut self.storage_endpoints {
            se.backend_from_config()?;
        }
        for repo in &mut self.repositories {
            repo.backend_from_config()?;
        }
        Ok(())
    }
}