use crate::canonical::settings_identity;
use crate::{
DefaultStatus, EngineDeclaration, EngineProfile, InvalidSettingReason, ProfileSelection,
ResolutionError, SettingApplicability, SettingDomain, SettingId, SettingLocation, SettingMap,
SettingScope, SettingValue, profiles_v1,
};
use animsmith_core::{
ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS, EngineContractError, InputIdentity, SourceFormatV1,
};
use std::collections::BTreeMap;
pub const SOURCE_TRANSFORM_PATH_MAX_BYTES: usize = 4_096;
pub fn lookup_profile(
selection: &ProfileSelection,
) -> Result<&'static EngineProfile, ResolutionError> {
profiles_v1()
.iter()
.find(|profile| profile.selection() == selection)
.ok_or_else(|| ResolutionError::UnknownProfile(selection.clone()))
}
pub fn resolve_static(
declaration: EngineDeclaration,
) -> Result<Option<StaticResolution>, ResolutionError> {
let EngineDeclaration {
selection,
document_settings,
clip_settings,
} = declaration;
let Some(selection) = selection else {
if document_settings.is_some() || !clip_settings.is_empty() {
return Err(ResolutionError::SettingsWithoutSelection);
}
return Ok(None);
};
let profile = lookup_profile(&selection)?;
let document_settings = validate_map(
profile,
document_settings.unwrap_or_default(),
SettingScope::Document,
SettingLocation::Document,
)?;
for descriptor in profile.setting_descriptors() {
if descriptor.applicability() == SettingApplicability::Applicable
&& descriptor.scope() == SettingScope::Document
&& descriptor.default_status() == DefaultStatus::RequiredWithoutDefault
&& !document_settings.contains_key(&descriptor.id())
{
return Err(ResolutionError::MissingRequiredSetting {
setting: descriptor.id(),
location: SettingLocation::Document,
});
}
}
let mut validated_clips = BTreeMap::new();
for (selector, settings) in clip_settings {
let values = validate_map(
profile,
settings,
SettingScope::Clip,
SettingLocation::ClipSelector(selector.clone()),
)?;
validated_clips.insert(selector, values);
}
Ok(Some(StaticResolution {
profile,
document_settings,
clip_overlays: validated_clips,
}))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StaticResolution {
profile: &'static EngineProfile,
document_settings: BTreeMap<SettingId, SettingValue>,
clip_overlays: BTreeMap<String, BTreeMap<SettingId, SettingValue>>,
}
impl StaticResolution {
pub const fn profile(&self) -> &'static EngineProfile {
self.profile
}
pub const fn document_settings(&self) -> &BTreeMap<SettingId, SettingValue> {
&self.document_settings
}
pub fn resolve_input(
&self,
source_format: SourceFormatV1,
clip_names: &[String],
) -> Result<ResolvedProfile, ResolutionError> {
self.resolve_input_iter(source_format, clip_names.iter().map(String::as_str))
}
pub fn resolve_input_iter<'a>(
&self,
source_format: SourceFormatV1,
clip_names: impl ExactSizeIterator<Item = &'a str>,
) -> Result<ResolvedProfile, ResolutionError> {
if !self.profile.accepted_inputs().contains(&source_format) {
return Err(ResolutionError::UnacceptedInputFormat {
selection: self.profile.selection().clone(),
format: source_format,
});
}
let clip_count = clip_names.len();
if clip_count > ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS {
return Err(ResolutionError::ResolvedSettingsContract(
EngineContractError::TooManyRows {
field: "settings.clips",
found: clip_count,
max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
},
));
}
let mut clips = Vec::with_capacity(clip_count);
for clip_name in clip_names {
let mut materialized = BTreeMap::new();
for (selector, settings) in &self.clip_overlays {
if selector != clip_name && animsmith_core::config::glob_match(selector, clip_name)
{
materialized.extend(settings.iter().map(|(id, value)| (*id, value.clone())));
}
}
if let Some(exact) = self.clip_overlays.get(clip_name) {
materialized.extend(exact.iter().map(|(id, value)| (*id, value.clone())));
}
for descriptor in self.profile.setting_descriptors() {
if descriptor.applicability() == SettingApplicability::Applicable
&& descriptor.scope() == SettingScope::Clip
&& descriptor.default_status() == DefaultStatus::RequiredWithoutDefault
&& !materialized.contains_key(&descriptor.id())
{
return Err(ResolutionError::MissingRequiredSetting {
setting: descriptor.id(),
location: SettingLocation::ClipSelector(clip_name.to_owned()),
});
}
}
clips.push(ResolvedClipSettings {
clip_name: clip_name.to_owned(),
settings: materialized,
});
}
clips.sort_by(|left, right| left.clip_name.cmp(&right.clip_name));
let identity = settings_identity(
self.profile,
&self.document_settings,
clips
.iter()
.map(|clip| (clip.clip_name.as_str(), &clip.settings)),
)?;
Ok(ResolvedProfile {
profile: self.profile,
source_format,
document_settings: self.document_settings.clone(),
clips,
settings_identity: identity,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedClipSettings {
clip_name: String,
settings: BTreeMap<SettingId, SettingValue>,
}
impl ResolvedClipSettings {
pub fn clip_name(&self) -> &str {
&self.clip_name
}
pub const fn settings(&self) -> &BTreeMap<SettingId, SettingValue> {
&self.settings
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedProfile {
profile: &'static EngineProfile,
source_format: SourceFormatV1,
document_settings: BTreeMap<SettingId, SettingValue>,
clips: Vec<ResolvedClipSettings>,
settings_identity: InputIdentity,
}
impl ResolvedProfile {
pub const fn profile(&self) -> &'static EngineProfile {
self.profile
}
pub const fn source_format(&self) -> SourceFormatV1 {
self.source_format
}
pub const fn document_settings(&self) -> &BTreeMap<SettingId, SettingValue> {
&self.document_settings
}
pub fn clip_settings(&self) -> &[ResolvedClipSettings] {
&self.clips
}
pub const fn settings_identity(&self) -> &InputIdentity {
&self.settings_identity
}
}
fn validate_map(
profile: &EngineProfile,
settings: SettingMap,
supplied_scope: SettingScope,
location: SettingLocation,
) -> Result<BTreeMap<SettingId, SettingValue>, ResolutionError> {
let mut validated = BTreeMap::new();
for (key, value) in settings {
let Some(id) = SettingId::from_str(&key) else {
return Err(ResolutionError::UnknownSetting {
key,
location: location.clone(),
});
};
let Some(descriptor) = profile.setting_descriptor(id) else {
return Err(ResolutionError::UnknownSetting {
key,
location: location.clone(),
});
};
if descriptor.applicability() == SettingApplicability::NotApplicable {
return Err(ResolutionError::NotApplicable {
setting: id,
location: location.clone(),
});
}
if descriptor.scope() != supplied_scope {
return Err(ResolutionError::WrongScope {
setting: id,
expected: descriptor.scope(),
found: supplied_scope,
location: location.clone(),
});
}
validate_value(id, descriptor.domain(), &value, location.clone())?;
validated.insert(id, value);
}
Ok(validated)
}
fn validate_value(
setting: SettingId,
domain: SettingDomain,
value: &SettingValue,
location: SettingLocation,
) -> Result<(), ResolutionError> {
let domain_matches = matches!(
(domain, value),
(SettingDomain::Boolean, SettingValue::Boolean(_))
| (SettingDomain::BakeOrExtract, SettingValue::BakeOrExtract(_))
| (
SettingDomain::SourceTransformPath,
SettingValue::SourceTransformPath(_)
)
);
if !domain_matches {
return Err(ResolutionError::InvalidSettingValue {
setting,
location,
reason: InvalidSettingReason::WrongDomain {
expected: domain,
found: value.kind(),
},
});
}
if let SettingValue::SourceTransformPath(path) = value {
let reason = if path.is_empty() {
Some(InvalidSettingReason::EmptyPath)
} else if path.len() > SOURCE_TRANSFORM_PATH_MAX_BYTES {
Some(InvalidSettingReason::PathTooLong {
bytes: path.len(),
limit: SOURCE_TRANSFORM_PATH_MAX_BYTES,
})
} else if path.starts_with('/') {
Some(InvalidSettingReason::AbsolutePath)
} else if path.chars().any(char::is_control) {
Some(InvalidSettingReason::ControlCharacter)
} else if path.split('/').any(str::is_empty) {
Some(InvalidSettingReason::EmptyPathSegment)
} else if path.split('/').any(|segment| matches!(segment, "." | "..")) {
Some(InvalidSettingReason::DotPathSegment)
} else {
None
};
if let Some(reason) = reason {
return Err(ResolutionError::InvalidSettingValue {
setting,
location,
reason,
});
}
}
Ok(())
}