use crate::cache::manager::CacheManager;
use crate::config::model::{ComponentConfig, ModelConfig, NamingConfig};
use anyhow::Result;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tracing::{debug, info};
pub mod caching;
pub mod coreml_metadata;
pub mod file_discovery;
pub mod manifest_parser;
pub mod schema_extractor;
pub mod shape_inference;
use caching::ConfigCaching;
use file_discovery::FileDiscovery;
use manifest_parser::ManifestParser;
use schema_extractor::SchemaExtractor;
use shape_inference::ShapeInference;
pub use schema_extractor::ComponentRole;
pub use coreml_metadata::CoreMLMetadataExtractor;
pub struct ConfigGenerator {
file_discovery: FileDiscovery,
manifest_parser: ManifestParser,
schema_extractor: SchemaExtractor,
shape_inference: ShapeInference,
caching: ConfigCaching,
#[allow(dead_code)]
metadata_extractor: CoreMLMetadataExtractor,
}
impl ConfigGenerator {
pub fn new() -> Result<Self> {
let cache_manager = CacheManager::new()?;
let caching = ConfigCaching::new(cache_manager);
Ok(Self {
file_discovery: FileDiscovery::new(),
manifest_parser: ManifestParser::new(),
schema_extractor: SchemaExtractor::new(),
shape_inference: ShapeInference::new(),
caching,
metadata_extractor: coreml_metadata::CoreMLMetadataExtractor::new(),
})
}
pub fn generate_config_from_directory_enhanced(
&self,
model_dir: &Path,
model_id: &str,
model_type: &str,
) -> Result<ModelConfig> {
info!("🔍 Generating config (enhanced) for model: {}", model_id);
debug!(" Model directory: {}", model_dir.display());
debug!(" Model type: {}", model_type);
self.file_discovery.validate_model_directory(model_dir)?;
let packages = self.file_discovery.find_coreml_packages(model_dir)?;
info!("📦 Found {} CoreML model files", packages.len());
for package in &packages {
debug!(
" • {}",
package.file_name().unwrap_or_default().to_string_lossy()
);
}
let mut components = HashMap::new();
for package_path in &packages {
self.process_package_with_metadata_detection(package_path, &mut components)?;
}
self.validate_required_components(&components)?;
let config = self
.build_model_config_enhanced(model_id, model_type, model_dir, components, &packages)?;
info!(
"✅ Generated enhanced config for {} with {} components",
model_id,
config.components.len()
);
self.caching.cache_config(model_id, &config)?;
Ok(config)
}
pub fn generate_config_from_directory(
&self,
model_dir: &Path,
model_id: &str,
model_type: &str,
) -> Result<ModelConfig> {
info!("🔍 Generating config for model: {}", model_id);
debug!(" Model directory: {}", model_dir.display());
debug!(" Model type: {}", model_type);
self.file_discovery.validate_model_directory(model_dir)?;
let packages = self.file_discovery.find_coreml_packages(model_dir)?;
info!("📦 Found {} CoreML model files", packages.len());
for package in &packages {
debug!(
" • {}",
package.file_name().unwrap_or_default().to_string_lossy()
);
}
let mut components = HashMap::new();
for package_path in &packages {
self.process_package(package_path, &mut components)?;
}
let config =
self.build_model_config(model_id, model_type, model_dir, components, &packages)?;
info!(
"✅ Generated config for {} with {} components",
model_id,
config.components.len()
);
self.caching.cache_config(model_id, &config)?;
Ok(config)
}
pub fn load_cached_config(&self, model_id: &str) -> Result<Option<ModelConfig>> {
self.caching.load_cached_config(model_id)
}
pub fn has_cached_config(&self, model_id: &str) -> bool {
self.caching.has_cached_config(model_id)
}
pub fn clear_cached_config(&self, model_id: &str) -> Result<()> {
self.caching.clear_cached_config(model_id)
}
fn process_package(
&self,
package_path: &Path,
components: &mut HashMap<String, ComponentConfig>,
) -> Result<()> {
let manifest = self.file_discovery.read_manifest(package_path)?;
let base_component_name = self.file_discovery.infer_component_name(package_path);
let parsed_components =
self.manifest_parser
.parse_package(package_path, &manifest, &base_component_name)?;
for (name, config) in parsed_components {
debug!(
"📋 Component '{}': inputs={:?} outputs={:?}",
name,
config.inputs.keys().collect::<Vec<_>>(),
config.outputs.keys().collect::<Vec<_>>()
);
components.insert(name, config);
}
Ok(())
}
fn process_package_with_metadata_detection(
&self,
package_path: &Path,
components: &mut HashMap<String, ComponentConfig>,
) -> Result<()> {
let manifest_source = self.file_discovery.find_manifest_source(package_path)?;
let manifest = self.file_discovery.read_manifest(package_path)?;
debug!("🔍 Processing package with source: {:?}", manifest_source);
let parsed_components = self.manifest_parser.parse_package_enhanced(
package_path,
&manifest_source,
&manifest,
&self.schema_extractor,
)?;
for (name, config) in parsed_components {
debug!(
"📋 Enhanced component '{}': inputs={:?} outputs={:?}",
name,
config.inputs.keys().collect::<Vec<_>>(),
config.outputs.keys().collect::<Vec<_>>()
);
components.insert(name, config);
}
Ok(())
}
fn validate_required_components(
&self,
components: &HashMap<String, ComponentConfig>,
) -> Result<()> {
if components.is_empty() {
return Err(anyhow::Error::msg("No components discovered"));
}
info!("✅ Component presence check passed (partial allowed in enhanced mode)");
Ok(())
}
fn build_model_config_enhanced(
&self,
model_id: &str,
model_type: &str,
model_dir: &Path,
components: HashMap<String, ComponentConfig>,
packages: &[PathBuf],
) -> Result<ModelConfig> {
let is_pipeline = packages.len() >= 3;
let shape_config = self
.shape_inference
.infer_shapes_with_schema_extractor(&components, &self.schema_extractor)?;
let naming_config = self.generate_naming_config(packages);
let component_list: Vec<(String, ComponentConfig)> = components.into_iter().collect();
let ffn_execution = self.manifest_parser.infer_execution_mode(&component_list);
info!("🔧 Detected execution mode: {}", ffn_execution);
let final_components: HashMap<String, ComponentConfig> =
component_list.into_iter().collect();
let model = ModelConfig {
model_info: crate::config::model::ModelInfo {
model_id: Some(model_id.to_string()),
path: Some(model_dir.to_string_lossy().to_string()),
model_type: model_type.to_string(),
discovered_at: Some(chrono::Utc::now().to_rfc3339()),
},
shapes: shape_config,
components: final_components,
naming: naming_config,
ffn_execution: Some(ffn_execution),
};
if is_pipeline {
let required = ["embeddings", "lm_head"];
let missing: Vec<_> = required
.iter()
.filter(|c| !model.components.contains_key(**c))
.collect();
if !missing.is_empty() {
return Err(anyhow::anyhow!(
"ModelConfig missing required components in pipeline: {:?}. Found: {:?}",
missing,
model.components.keys().collect::<Vec<_>>()
));
}
}
Ok(model)
}
fn build_model_config(
&self,
model_id: &str,
model_type: &str,
model_dir: &Path,
components: HashMap<String, ComponentConfig>,
packages: &[PathBuf],
) -> Result<ModelConfig> {
let shape_config = self.shape_inference.infer_shapes(&components)?;
let naming_config = self.generate_naming_config(packages);
let component_list: Vec<(String, ComponentConfig)> = components.into_iter().collect();
let ffn_execution = self.manifest_parser.infer_execution_mode(&component_list);
info!("🔧 Detected execution mode: {}", ffn_execution);
let final_components: HashMap<String, ComponentConfig> =
component_list.into_iter().collect();
Ok(ModelConfig {
model_info: crate::config::model::ModelInfo {
model_id: Some(model_id.to_string()),
path: Some(model_dir.to_string_lossy().to_string()),
model_type: model_type.to_string(),
discovered_at: Some(chrono::Utc::now().to_rfc3339()),
},
shapes: shape_config,
components: final_components,
naming: naming_config,
ffn_execution: Some(ffn_execution),
})
}
fn generate_naming_config(&self, _packages: &[PathBuf]) -> NamingConfig {
NamingConfig {
embeddings_pattern: None,
ffn_infer_pattern: None,
ffn_prefill_pattern: None,
lm_head_pattern: None,
}
}
}
impl ConfigGenerator {
pub fn find_mlpackage_files(&self, model_dir: &Path) -> Result<Vec<PathBuf>> {
self.file_discovery.find_coreml_packages(model_dir)
}
pub fn infer_component_name_from_file(&self, package_path: &Path) -> String {
self.file_discovery.infer_component_name(package_path)
}
pub fn analyze_mlpackage(&self, package_path: &Path) -> Result<ComponentConfig> {
let manifest = self.file_discovery.read_manifest(package_path)?;
let inputs = self.schema_extractor.extract_inputs(&manifest)?;
let outputs = self.schema_extractor.extract_outputs(&manifest)?;
Ok(ComponentConfig {
file_path: Some(package_path.to_string_lossy().to_string()),
inputs,
outputs,
functions: Vec::new(),
input_order: None,
})
}
pub fn extract_function_based_components(
&self,
package_path: &Path,
_base_config: &ComponentConfig,
) -> Result<Option<HashMap<String, ComponentConfig>>> {
let manifest = self.file_discovery.read_manifest(package_path)?;
let base_component_name = self.file_discovery.infer_component_name(package_path);
let parsed_components =
self.manifest_parser
.parse_package(package_path, &manifest, &base_component_name)?;
if parsed_components.len() > 1 {
let function_components: HashMap<String, ComponentConfig> =
parsed_components.into_iter().collect();
Ok(Some(function_components))
} else {
Ok(None)
}
}
pub fn parse_tensor_configs_from_schema(
&self,
schema: &[serde_json::Value],
) -> Result<HashMap<String, crate::model_config::TensorConfig>> {
self.schema_extractor.parse_tensor_configs(schema)
}
pub fn compute_shape_info_generic(
&self,
components: &HashMap<String, ComponentConfig>,
) -> Result<crate::model_config::ShapeConfig> {
self.shape_inference.infer_shapes(components)
}
pub fn generate_naming_config_generic(&self, packages: &[PathBuf]) -> NamingConfig {
self.generate_naming_config(packages)
}
pub fn determine_execution_mode_generic(
&self,
components: &HashMap<String, ComponentConfig>,
) -> String {
let component_list: Vec<(String, ComponentConfig)> = components
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
self.manifest_parser.infer_execution_mode(&component_list)
}
pub fn cache_generated_config(&self, model_id: &str, config: &ModelConfig) -> Result<()> {
self.caching.cache_config(model_id, config)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn create_mock_mlpackage(temp_dir: &Path, name: &str) -> Result<PathBuf> {
let package_path = temp_dir.join(format!("{name}.mlpackage"));
std::fs::create_dir_all(&package_path)?;
let manifest = serde_json::json!({
"fileFormatVersion": "1.0.0",
"itemInfoEntries": [
{
"path": "model.mlmodel",
"digestType": "SHA256"
}
]
});
let manifest_path = package_path.join("Manifest.json");
std::fs::write(manifest_path, serde_json::to_string_pretty(&manifest)?)?;
let model_path = package_path.join("model.mlmodel");
std::fs::write(model_path, b"mock model data")?;
Ok(package_path)
}
#[test]
fn test_modular_config_generator_creation() -> Result<()> {
let generator = ConfigGenerator::new()?;
assert!(!generator.caching.has_cached_config("nonexistent"));
Ok(())
}
#[test]
fn test_modular_file_discovery() -> Result<()> {
let temp_dir = TempDir::new()?;
let generator = ConfigGenerator::new()?;
create_mock_mlpackage(temp_dir.path(), "embeddings")?;
create_mock_mlpackage(temp_dir.path(), "transformer")?;
create_mock_mlpackage(temp_dir.path(), "head")?;
let packages = generator
.file_discovery
.find_coreml_packages(temp_dir.path())?;
assert_eq!(packages.len(), 3);
assert!(packages.iter().any(|p| p
.file_name()
.unwrap()
.to_string_lossy()
.contains("embeddings")));
assert!(packages.iter().any(|p| p
.file_name()
.unwrap()
.to_string_lossy()
.contains("transformer")));
assert!(packages
.iter()
.any(|p| p.file_name().unwrap().to_string_lossy().contains("head")));
Ok(())
}
#[test]
fn test_enhanced_parsing_falls_back_to_filename_when_metadata_empty() -> Result<()> {
let temp_dir = TempDir::new()?;
let generator = ConfigGenerator::new()?;
let pkg_path = create_mock_mlpackage(temp_dir.path(), "embeddings")?;
let mut components = std::collections::HashMap::new();
generator
.process_package_with_metadata_detection(&pkg_path, &mut components)
.expect("enhanced parsing should not fail");
assert!(
components.contains_key("embeddings"),
"expected 'embeddings' component, got: {:?}",
components.keys().collect::<Vec<_>>()
);
Ok(())
}
}