1use std::collections::HashMap;
2use std::fmt;
3use std::sync::Arc;
4
5use crate::detail::api::{to_cstring, Api, Kvps};
6use crate::detail::ffi::*;
7use crate::error::{FoundryLocalError, Result};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum LogLevel {
12 Trace,
13 Debug,
14 Info,
15 Warn,
16 Error,
17 Fatal,
18}
19
20impl LogLevel {
21 fn as_native(&self) -> flLogLevel {
23 match self {
24 Self::Trace => FOUNDRY_LOCAL_LOG_VERBOSE,
25 Self::Debug => FOUNDRY_LOCAL_LOG_DEBUG,
26 Self::Info => FOUNDRY_LOCAL_LOG_INFO,
27 Self::Warn => FOUNDRY_LOCAL_LOG_WARNING,
28 Self::Error => FOUNDRY_LOCAL_LOG_ERROR,
29 Self::Fatal => FOUNDRY_LOCAL_LOG_FATAL,
30 }
31 }
32}
33
34pub trait Logger: Send + Sync {
39 fn log(&self, level: LogLevel, message: &str);
41}
42
43#[derive(Default)]
54pub struct FoundryLocalConfig {
55 app_name: String,
56 app_data_dir: Option<String>,
57 model_cache_dir: Option<String>,
58 logs_dir: Option<String>,
59 log_level: Option<LogLevel>,
60 catalog_urls: Vec<(String, Option<String>)>,
61 catalog_region: Option<String>,
62 web_service_urls: Option<String>,
63 service_endpoint: Option<String>,
64 library_path: Option<String>,
65 additional_settings: Option<HashMap<String, String>>,
66 logger: Option<Box<dyn Logger>>,
67}
68
69impl fmt::Debug for FoundryLocalConfig {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 f.debug_struct("FoundryLocalConfig")
72 .field("app_name", &self.app_name)
73 .field("app_data_dir", &self.app_data_dir)
74 .field("model_cache_dir", &self.model_cache_dir)
75 .field("logs_dir", &self.logs_dir)
76 .field("log_level", &self.log_level)
77 .field("catalog_urls", &self.catalog_urls)
78 .field("catalog_region", &self.catalog_region)
79 .field("web_service_urls", &self.web_service_urls)
80 .field("service_endpoint", &self.service_endpoint)
81 .field("library_path", &self.library_path)
82 .field("additional_settings", &self.additional_settings)
83 .field("logger", &self.logger.as_ref().map(|_| ".."))
84 .finish()
85 }
86}
87
88impl FoundryLocalConfig {
89 pub fn new(app_name: impl Into<String>) -> Self {
100 Self {
101 app_name: app_name.into(),
102 ..Self::default()
103 }
104 }
105
106 pub fn app_data_dir(mut self, dir: impl Into<String>) -> Self {
108 self.app_data_dir = Some(dir.into());
109 self
110 }
111
112 pub fn model_cache_dir(mut self, dir: impl Into<String>) -> Self {
114 self.model_cache_dir = Some(dir.into());
115 self
116 }
117
118 pub fn logs_dir(mut self, dir: impl Into<String>) -> Self {
120 self.logs_dir = Some(dir.into());
121 self
122 }
123
124 pub fn log_level(mut self, level: LogLevel) -> Self {
126 self.log_level = Some(level);
127 self
128 }
129
130 pub fn catalog_url(mut self, url: impl Into<String>) -> Self {
137 self.catalog_urls.push((url.into(), None));
138 self
139 }
140
141 pub fn catalog_url_with_filter(
146 mut self,
147 url: impl Into<String>,
148 filter_override: impl Into<String>,
149 ) -> Self {
150 self.catalog_urls
151 .push((url.into(), Some(filter_override.into())));
152 self
153 }
154
155 pub fn catalog_region(mut self, region: impl Into<String>) -> Self {
157 self.catalog_region = Some(region.into());
158 self
159 }
160
161 pub fn web_service_urls(mut self, urls: impl Into<String>) -> Self {
163 self.web_service_urls = Some(urls.into());
164 self
165 }
166
167 pub fn service_endpoint(mut self, endpoint: impl Into<String>) -> Self {
169 self.service_endpoint = Some(endpoint.into());
170 self
171 }
172
173 pub fn library_path(mut self, path: impl Into<String>) -> Self {
175 self.library_path = Some(path.into());
176 self
177 }
178
179 pub fn additional_setting(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
181 self.additional_settings
182 .get_or_insert_with(HashMap::new)
183 .insert(key.into(), value.into());
184 self
185 }
186
187 pub fn logger(mut self, logger: impl Logger + 'static) -> Self {
200 self.logger = Some(Box::new(logger));
201 self
202 }
203
204 pub(crate) fn library_path_ref(&self) -> Option<&str> {
208 self.library_path.as_deref()
209 }
210
211 pub(crate) fn take_logger(&mut self) -> Option<Box<dyn Logger>> {
213 self.logger.take()
214 }
215
216 pub(crate) fn build_native(&self, api: &Arc<Api>) -> Result<NativeConfig> {
221 let app_name = self.app_name.trim();
222 if app_name.is_empty() {
223 return Err(FoundryLocalError::InvalidConfiguration {
224 reason: "app_name must be set and non-empty".into(),
225 });
226 }
227
228 let cfg = NativeConfig::create(Arc::clone(api), app_name)?;
229 let c = api.config_api();
230
231 if let Some(dir) = &self.app_data_dir {
232 let s = to_cstring(dir)?;
233 api.check(unsafe { (c.SetAppDataDir)(cfg.ptr, s.as_ptr()) })?;
235 }
236 if let Some(dir) = &self.model_cache_dir {
237 let s = to_cstring(dir)?;
238 api.check(unsafe { (c.SetModelCacheDir)(cfg.ptr, s.as_ptr()) })?;
239 }
240 if let Some(dir) = &self.logs_dir {
241 let s = to_cstring(dir)?;
242 api.check(unsafe { (c.SetLogsDir)(cfg.ptr, s.as_ptr()) })?;
243 }
244 if let Some(level) = self.log_level {
245 api.check(unsafe { (c.SetDefaultLogLevel)(cfg.ptr, level.as_native()) })?;
246 }
247 for (url, filter_override) in &self.catalog_urls {
248 let url = to_cstring(url)?;
249 let filter_override = filter_override.as_deref().map(to_cstring).transpose()?;
250 let filter_override_ptr = filter_override
251 .as_ref()
252 .map_or(std::ptr::null(), |filter| filter.as_ptr());
253 api.check(unsafe { (c.AddCatalogUrl)(cfg.ptr, url.as_ptr(), filter_override_ptr) })?;
254 }
255 if let Some(region) = &self.catalog_region {
256 let region = to_cstring(region)?;
257 api.check(unsafe { (c.SetCatalogRegion)(cfg.ptr, region.as_ptr()) })?;
258 }
259 if let Some(urls) = &self.web_service_urls {
260 for url in urls.split(',').map(str::trim).filter(|u| !u.is_empty()) {
261 let s = to_cstring(url)?;
262 api.check(unsafe { (c.AddWebServiceEndpoint)(cfg.ptr, s.as_ptr()) })?;
263 }
264 }
265 if let Some(endpoint) = &self.service_endpoint {
266 let s = to_cstring(endpoint)?;
267 api.check(unsafe { (c.SetExternalServiceUrl)(cfg.ptr, s.as_ptr()) })?;
268 }
269 if let Some(extra) = &self.additional_settings {
270 if !extra.is_empty() {
271 let kvps = Kvps::from_pairs(Arc::clone(api), extra.iter())?;
272 api.check(unsafe { (c.SetAdditionalOptions)(cfg.ptr, kvps.as_ptr()) })?;
273 }
274 }
275
276 Ok(cfg)
277 }
278}
279
280pub(crate) struct NativeConfig {
282 api: Arc<Api>,
283 ptr: *mut flConfiguration,
284}
285
286impl NativeConfig {
287 fn create(api: Arc<Api>, app_name: &str) -> Result<Self> {
288 let name = to_cstring(app_name)?;
289 let mut ptr: *mut flConfiguration = std::ptr::null_mut();
290 let status = unsafe { (api.config_api().Create)(name.as_ptr(), &mut ptr) };
292 api.check(status)?;
293 Ok(Self { api, ptr })
294 }
295
296 pub(crate) fn as_ptr(&self) -> *const flConfiguration {
297 self.ptr
298 }
299}
300
301impl Drop for NativeConfig {
302 fn drop(&mut self) {
303 if !self.ptr.is_null() {
304 unsafe { (self.api.config_api().Configuration_Release)(self.ptr) };
306 self.ptr = std::ptr::null_mut();
307 }
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn catalog_builder_preserves_urls_filters_and_priority() {
317 let config = FoundryLocalConfig::new("test")
318 .catalog_url("https://first.example/catalog")
319 .catalog_url_with_filter("https://second.example/catalog", "device=cpu");
320
321 assert_eq!(
322 config.catalog_urls,
323 vec![
324 ("https://first.example/catalog".into(), None),
325 (
326 "https://second.example/catalog".into(),
327 Some("device=cpu".into())
328 ),
329 ]
330 );
331 }
332
333 #[test]
334 fn catalog_region_is_optional_and_set_by_builder() {
335 assert_eq!(FoundryLocalConfig::new("test").catalog_region, None);
336 assert_eq!(
337 FoundryLocalConfig::new("test")
338 .catalog_region("australiaeast")
339 .catalog_region,
340 Some("australiaeast".into())
341 );
342 }
343}