use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
use crate::detail::api::{to_cstring, Api, Kvps};
use crate::detail::ffi::*;
use crate::error::{FoundryLocalError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
Fatal,
}
impl LogLevel {
fn as_native(&self) -> flLogLevel {
match self {
Self::Trace => FOUNDRY_LOCAL_LOG_VERBOSE,
Self::Debug => FOUNDRY_LOCAL_LOG_DEBUG,
Self::Info => FOUNDRY_LOCAL_LOG_INFO,
Self::Warn => FOUNDRY_LOCAL_LOG_WARNING,
Self::Error => FOUNDRY_LOCAL_LOG_ERROR,
Self::Fatal => FOUNDRY_LOCAL_LOG_FATAL,
}
}
}
pub trait Logger: Send + Sync {
fn log(&self, level: LogLevel, message: &str);
}
#[derive(Default)]
pub struct FoundryLocalConfig {
app_name: String,
app_data_dir: Option<String>,
model_cache_dir: Option<String>,
logs_dir: Option<String>,
log_level: Option<LogLevel>,
catalog_urls: Vec<(String, Option<String>)>,
catalog_region: Option<String>,
web_service_urls: Option<String>,
service_endpoint: Option<String>,
library_path: Option<String>,
additional_settings: Option<HashMap<String, String>>,
logger: Option<Box<dyn Logger>>,
}
impl fmt::Debug for FoundryLocalConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FoundryLocalConfig")
.field("app_name", &self.app_name)
.field("app_data_dir", &self.app_data_dir)
.field("model_cache_dir", &self.model_cache_dir)
.field("logs_dir", &self.logs_dir)
.field("log_level", &self.log_level)
.field("catalog_urls", &self.catalog_urls)
.field("catalog_region", &self.catalog_region)
.field("web_service_urls", &self.web_service_urls)
.field("service_endpoint", &self.service_endpoint)
.field("library_path", &self.library_path)
.field("additional_settings", &self.additional_settings)
.field("logger", &self.logger.as_ref().map(|_| ".."))
.finish()
}
}
impl FoundryLocalConfig {
pub fn new(app_name: impl Into<String>) -> Self {
Self {
app_name: app_name.into(),
..Self::default()
}
}
pub fn app_data_dir(mut self, dir: impl Into<String>) -> Self {
self.app_data_dir = Some(dir.into());
self
}
pub fn model_cache_dir(mut self, dir: impl Into<String>) -> Self {
self.model_cache_dir = Some(dir.into());
self
}
pub fn logs_dir(mut self, dir: impl Into<String>) -> Self {
self.logs_dir = Some(dir.into());
self
}
pub fn log_level(mut self, level: LogLevel) -> Self {
self.log_level = Some(level);
self
}
pub fn catalog_url(mut self, url: impl Into<String>) -> Self {
self.catalog_urls.push((url.into(), None));
self
}
pub fn catalog_url_with_filter(
mut self,
url: impl Into<String>,
filter_override: impl Into<String>,
) -> Self {
self.catalog_urls
.push((url.into(), Some(filter_override.into())));
self
}
pub fn catalog_region(mut self, region: impl Into<String>) -> Self {
self.catalog_region = Some(region.into());
self
}
pub fn web_service_urls(mut self, urls: impl Into<String>) -> Self {
self.web_service_urls = Some(urls.into());
self
}
pub fn service_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.service_endpoint = Some(endpoint.into());
self
}
pub fn library_path(mut self, path: impl Into<String>) -> Self {
self.library_path = Some(path.into());
self
}
pub fn additional_setting(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.additional_settings
.get_or_insert_with(HashMap::new)
.insert(key.into(), value.into());
self
}
pub fn logger(mut self, logger: impl Logger + 'static) -> Self {
self.logger = Some(Box::new(logger));
self
}
pub(crate) fn library_path_ref(&self) -> Option<&str> {
self.library_path.as_deref()
}
pub(crate) fn take_logger(&mut self) -> Option<Box<dyn Logger>> {
self.logger.take()
}
pub(crate) fn build_native(&self, api: &Arc<Api>) -> Result<NativeConfig> {
let app_name = self.app_name.trim();
if app_name.is_empty() {
return Err(FoundryLocalError::InvalidConfiguration {
reason: "app_name must be set and non-empty".into(),
});
}
let cfg = NativeConfig::create(Arc::clone(api), app_name)?;
let c = api.config_api();
if let Some(dir) = &self.app_data_dir {
let s = to_cstring(dir)?;
api.check(unsafe { (c.SetAppDataDir)(cfg.ptr, s.as_ptr()) })?;
}
if let Some(dir) = &self.model_cache_dir {
let s = to_cstring(dir)?;
api.check(unsafe { (c.SetModelCacheDir)(cfg.ptr, s.as_ptr()) })?;
}
if let Some(dir) = &self.logs_dir {
let s = to_cstring(dir)?;
api.check(unsafe { (c.SetLogsDir)(cfg.ptr, s.as_ptr()) })?;
}
if let Some(level) = self.log_level {
api.check(unsafe { (c.SetDefaultLogLevel)(cfg.ptr, level.as_native()) })?;
}
for (url, filter_override) in &self.catalog_urls {
let url = to_cstring(url)?;
let filter_override = filter_override.as_deref().map(to_cstring).transpose()?;
let filter_override_ptr = filter_override
.as_ref()
.map_or(std::ptr::null(), |filter| filter.as_ptr());
api.check(unsafe { (c.AddCatalogUrl)(cfg.ptr, url.as_ptr(), filter_override_ptr) })?;
}
if let Some(region) = &self.catalog_region {
let region = to_cstring(region)?;
api.check(unsafe { (c.SetCatalogRegion)(cfg.ptr, region.as_ptr()) })?;
}
if let Some(urls) = &self.web_service_urls {
for url in urls.split(',').map(str::trim).filter(|u| !u.is_empty()) {
let s = to_cstring(url)?;
api.check(unsafe { (c.AddWebServiceEndpoint)(cfg.ptr, s.as_ptr()) })?;
}
}
if let Some(endpoint) = &self.service_endpoint {
let s = to_cstring(endpoint)?;
api.check(unsafe { (c.SetExternalServiceUrl)(cfg.ptr, s.as_ptr()) })?;
}
if let Some(extra) = &self.additional_settings {
if !extra.is_empty() {
let kvps = Kvps::from_pairs(Arc::clone(api), extra.iter())?;
api.check(unsafe { (c.SetAdditionalOptions)(cfg.ptr, kvps.as_ptr()) })?;
}
}
Ok(cfg)
}
}
pub(crate) struct NativeConfig {
api: Arc<Api>,
ptr: *mut flConfiguration,
}
impl NativeConfig {
fn create(api: Arc<Api>, app_name: &str) -> Result<Self> {
let name = to_cstring(app_name)?;
let mut ptr: *mut flConfiguration = std::ptr::null_mut();
let status = unsafe { (api.config_api().Create)(name.as_ptr(), &mut ptr) };
api.check(status)?;
Ok(Self { api, ptr })
}
pub(crate) fn as_ptr(&self) -> *const flConfiguration {
self.ptr
}
}
impl Drop for NativeConfig {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe { (self.api.config_api().Configuration_Release)(self.ptr) };
self.ptr = std::ptr::null_mut();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn catalog_builder_preserves_urls_filters_and_priority() {
let config = FoundryLocalConfig::new("test")
.catalog_url("https://first.example/catalog")
.catalog_url_with_filter("https://second.example/catalog", "device=cpu");
assert_eq!(
config.catalog_urls,
vec![
("https://first.example/catalog".into(), None),
(
"https://second.example/catalog".into(),
Some("device=cpu".into())
),
]
);
}
#[test]
fn catalog_region_is_optional_and_set_by_builder() {
assert_eq!(FoundryLocalConfig::new("test").catalog_region, None);
assert_eq!(
FoundryLocalConfig::new("test")
.catalog_region("australiaeast")
.catalog_region,
Some("australiaeast".into())
);
}
}