pixelflow-core 0.1.0

Core abstractions shared by PixelFlow crates.
Documentation
//! Core runtime owner for per-instance registries and plugin loading.

use std::path::PathBuf;

use crate::plugin_host::{load_plugins_from_directories, platform_plugin_directories};
use crate::{
    FilterRegistry, LoadedPlugin, Logger, OrderedRender, RenderEngine, RenderExecutorMap,
    RenderOptions, Result,
};

/// Configuration used when creating a [`Core`].
#[derive(Clone)]
pub struct CoreConfig {
    auto_load_plugins: bool,
    plugin_directories: Vec<PathBuf>,
    logger: Logger,
    worker_threads: usize,
}

impl Default for CoreConfig {
    fn default() -> Self {
        Self {
            auto_load_plugins: true,
            plugin_directories: Vec::new(),
            logger: Logger::default(),
            worker_threads: std::thread::available_parallelism().map_or(1, usize::from),
        }
    }
}

impl CoreConfig {
    /// Enables or disables conventional plugin directory scanning.
    #[must_use]
    pub const fn with_auto_load_plugins(mut self, enabled: bool) -> Self {
        self.auto_load_plugins = enabled;
        self
    }

    /// Replaces logger used for plugin diagnostics.
    #[must_use]
    pub fn with_logger(mut self, logger: Logger) -> Self {
        self.logger = logger;
        self
    }

    /// Sets per-core worker thread count. Zero becomes one.
    #[must_use]
    pub fn with_worker_threads(mut self, worker_threads: usize) -> Self {
        self.worker_threads = worker_threads.max(1);
        self
    }

    /// Returns mutable plugin directory overrides.
    #[must_use]
    pub const fn plugin_directories_mut(&mut self) -> &mut Vec<PathBuf> {
        &mut self.plugin_directories
    }

    pub(crate) const fn auto_load_plugins(&self) -> bool {
        self.auto_load_plugins
    }

    pub(crate) fn plugin_directories(&self) -> &[PathBuf] {
        &self.plugin_directories
    }

    pub(crate) const fn logger(&self) -> &Logger {
        &self.logger
    }

    /// Returns configured per-core worker thread count.
    #[must_use]
    pub const fn worker_threads(&self) -> usize {
        self.worker_threads
    }
}

/// Per-instance PixelFlow host state.
pub struct Core {
    registry: FilterRegistry,
    config: CoreConfig,
    loaded_plugins: Vec<LoadedPlugin>,
}

impl Core {
    /// Creates a core using default configuration.
    pub fn new() -> Result<Self> {
        Self::with_config(CoreConfig::default())
    }

    /// Creates a core using explicit configuration.
    pub fn with_config(config: CoreConfig) -> Result<Self> {
        let mut registry = FilterRegistry::new();
        let mut directories = config.plugin_directories().to_vec();
        if config.auto_load_plugins() {
            directories.extend(platform_plugin_directories());
        }
        let loaded_plugins =
            load_plugins_from_directories(&directories, &mut registry, config.logger());

        Ok(Self {
            registry,
            config,
            loaded_plugins,
        })
    }

    /// Returns immutable registry access.
    #[must_use]
    pub const fn registry(&self) -> &FilterRegistry {
        &self.registry
    }

    /// Returns mutable registry access for in-process registration.
    pub const fn registry_mut(&mut self) -> &mut FilterRegistry {
        &mut self.registry
    }

    /// Returns core configuration.
    #[must_use]
    pub const fn config(&self) -> &CoreConfig {
        &self.config
    }

    /// Returns plugins loaded during core construction.
    #[must_use]
    pub fn loaded_plugins(&self) -> &[LoadedPlugin] {
        &self.loaded_plugins
    }

    /// Creates render engine using this core's worker configuration.
    #[must_use]
    pub const fn render_engine(&self) -> RenderEngine {
        RenderEngine::new(crate::WorkerPoolConfig::new(self.config.worker_threads()))
    }

    /// Starts blocking ordered rendering using this core's worker configuration.
    pub fn render_ordered(
        &self,
        graph: crate::Graph,
        executors: RenderExecutorMap,
        options: RenderOptions,
    ) -> Result<OrderedRender> {
        self.render_engine()
            .render_ordered(graph, executors, options)
    }
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use crate::{Core, CoreConfig, FilterDescriptor};

    #[test]
    fn core_can_disable_plugin_auto_load_for_deterministic_tests() {
        let core = Core::with_config(CoreConfig::default().with_auto_load_plugins(false))
            .expect("core should construct without scanning plugin directories");

        assert!(core.registry().filter_names().is_empty());
    }

    #[test]
    fn core_preserves_pre_registered_filters_when_plugin_load_fails() {
        let mut config = CoreConfig::default().with_auto_load_plugins(false);
        config
            .plugin_directories_mut()
            .push(PathBuf::from("/path/that/does/not/exist"));
        let mut core = Core::with_config(config).expect("core should construct");

        core.registry_mut()
            .register_filter(FilterDescriptor::new("crop", "pixelflow", "crop"))
            .expect("built-in filter should register");

        assert!(core.registry().contains_filter("crop"));
    }

    #[test]
    fn core_config_defaults_to_available_worker_threads() {
        let config = CoreConfig::default();

        assert!(config.worker_threads() >= 1);
    }

    #[test]
    fn core_config_accepts_explicit_worker_count_for_cli() {
        let config = CoreConfig::default().with_worker_threads(3);

        assert_eq!(config.worker_threads(), 3);
    }

    #[test]
    fn core_config_clamps_zero_workers_to_one() {
        let config = CoreConfig::default().with_worker_threads(0);

        assert_eq!(config.worker_threads(), 1);
    }

    #[test]
    fn core_render_ordered_uses_configured_worker_count() {
        let core = Core::with_config(
            CoreConfig::default()
                .with_auto_load_plugins(false)
                .with_worker_threads(1),
        )
        .expect("core should construct");

        assert_eq!(core.render_engine().worker_threads(), 1);
    }
}