1use std::path::PathBuf;
4
5use crate::plugin_host::{load_plugins_from_directories, platform_plugin_directories};
6use crate::{
7 FilterRegistry, LoadedPlugin, Logger, OrderedRender, RenderEngine, RenderExecutorMap,
8 RenderOptions, Result,
9};
10
11#[derive(Clone)]
13pub struct CoreConfig {
14 auto_load_plugins: bool,
15 plugin_directories: Vec<PathBuf>,
16 logger: Logger,
17 worker_threads: usize,
18}
19
20impl Default for CoreConfig {
21 fn default() -> Self {
22 Self {
23 auto_load_plugins: true,
24 plugin_directories: Vec::new(),
25 logger: Logger::default(),
26 worker_threads: std::thread::available_parallelism().map_or(1, usize::from),
27 }
28 }
29}
30
31impl CoreConfig {
32 #[must_use]
34 pub const fn with_auto_load_plugins(mut self, enabled: bool) -> Self {
35 self.auto_load_plugins = enabled;
36 self
37 }
38
39 #[must_use]
41 pub fn with_logger(mut self, logger: Logger) -> Self {
42 self.logger = logger;
43 self
44 }
45
46 #[must_use]
48 pub fn with_worker_threads(mut self, worker_threads: usize) -> Self {
49 self.worker_threads = worker_threads.max(1);
50 self
51 }
52
53 #[must_use]
55 pub const fn plugin_directories_mut(&mut self) -> &mut Vec<PathBuf> {
56 &mut self.plugin_directories
57 }
58
59 pub(crate) const fn auto_load_plugins(&self) -> bool {
60 self.auto_load_plugins
61 }
62
63 pub(crate) fn plugin_directories(&self) -> &[PathBuf] {
64 &self.plugin_directories
65 }
66
67 pub(crate) const fn logger(&self) -> &Logger {
68 &self.logger
69 }
70
71 #[must_use]
73 pub const fn worker_threads(&self) -> usize {
74 self.worker_threads
75 }
76}
77
78pub struct Core {
80 registry: FilterRegistry,
81 config: CoreConfig,
82 loaded_plugins: Vec<LoadedPlugin>,
83}
84
85impl Core {
86 pub fn new() -> Result<Self> {
88 Self::with_config(CoreConfig::default())
89 }
90
91 pub fn with_config(config: CoreConfig) -> Result<Self> {
93 let mut registry = FilterRegistry::new();
94 let mut directories = config.plugin_directories().to_vec();
95 if config.auto_load_plugins() {
96 directories.extend(platform_plugin_directories());
97 }
98 let loaded_plugins =
99 load_plugins_from_directories(&directories, &mut registry, config.logger());
100
101 Ok(Self {
102 registry,
103 config,
104 loaded_plugins,
105 })
106 }
107
108 #[must_use]
110 pub const fn registry(&self) -> &FilterRegistry {
111 &self.registry
112 }
113
114 pub const fn registry_mut(&mut self) -> &mut FilterRegistry {
116 &mut self.registry
117 }
118
119 #[must_use]
121 pub const fn config(&self) -> &CoreConfig {
122 &self.config
123 }
124
125 #[must_use]
127 pub fn loaded_plugins(&self) -> &[LoadedPlugin] {
128 &self.loaded_plugins
129 }
130
131 #[must_use]
133 pub const fn render_engine(&self) -> RenderEngine {
134 RenderEngine::new(crate::WorkerPoolConfig::new(self.config.worker_threads()))
135 }
136
137 pub fn render_ordered(
139 &self,
140 graph: crate::Graph,
141 executors: RenderExecutorMap,
142 options: RenderOptions,
143 ) -> Result<OrderedRender> {
144 self.render_engine()
145 .render_ordered(graph, executors, options)
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use std::path::PathBuf;
152
153 use crate::{Core, CoreConfig, FilterDescriptor};
154
155 #[test]
156 fn core_can_disable_plugin_auto_load_for_deterministic_tests() {
157 let core = Core::with_config(CoreConfig::default().with_auto_load_plugins(false))
158 .expect("core should construct without scanning plugin directories");
159
160 assert!(core.registry().filter_names().is_empty());
161 }
162
163 #[test]
164 fn core_preserves_pre_registered_filters_when_plugin_load_fails() {
165 let mut config = CoreConfig::default().with_auto_load_plugins(false);
166 config
167 .plugin_directories_mut()
168 .push(PathBuf::from("/path/that/does/not/exist"));
169 let mut core = Core::with_config(config).expect("core should construct");
170
171 core.registry_mut()
172 .register_filter(FilterDescriptor::new("crop", "pixelflow", "crop"))
173 .expect("built-in filter should register");
174
175 assert!(core.registry().contains_filter("crop"));
176 }
177
178 #[test]
179 fn core_config_defaults_to_available_worker_threads() {
180 let config = CoreConfig::default();
181
182 assert!(config.worker_threads() >= 1);
183 }
184
185 #[test]
186 fn core_config_accepts_explicit_worker_count_for_cli() {
187 let config = CoreConfig::default().with_worker_threads(3);
188
189 assert_eq!(config.worker_threads(), 3);
190 }
191
192 #[test]
193 fn core_config_clamps_zero_workers_to_one() {
194 let config = CoreConfig::default().with_worker_threads(0);
195
196 assert_eq!(config.worker_threads(), 1);
197 }
198
199 #[test]
200 fn core_render_ordered_uses_configured_worker_count() {
201 let core = Core::with_config(
202 CoreConfig::default()
203 .with_auto_load_plugins(false)
204 .with_worker_threads(1),
205 )
206 .expect("core should construct");
207
208 assert_eq!(core.render_engine().worker_threads(), 1);
209 }
210}