use std::path::{Path, PathBuf};
use crate::error::Result;
use crate::error::RustmotionError;
use crate::schema::{
AnimatedBackground, BackgroundEntry, BackgroundPreset, BackgroundValue, EasingType,
IncludeDirective, ResolvedBackground, ResolvedScenario, ResolvedView, Scenario, Scene,
SceneEntry, ViewType,
};
const MAX_INCLUDE_DEPTH: u8 = 8;
pub enum IncludeSource {
File(PathBuf),
Inline,
}
pub fn resolve_includes(scenario: Scenario, source: &IncludeSource) -> Result<ResolvedScenario> {
let mut audio = scenario.audio;
let mut included_paths = Vec::new();
let has_scenes = !scenario.scenes.is_empty();
let has_composition = scenario.composition.is_some();
if has_scenes && has_composition {
return Err(RustmotionError::CompositionAndScenesConflict);
}
let templates = &scenario.backgrounds;
let views = if let Some(composition) = scenario.composition {
let mut views = Vec::with_capacity(composition.len());
for view in composition {
let mut scenes =
resolve_entries(view.scenes, source, 0, &mut audio, &mut included_paths)?;
for scene in &mut scenes {
resolve_scene_background(scene, templates)?;
}
let view_bg = resolve_background_value(
view.background.as_ref(),
&view.animated_background,
templates,
)?;
views.push(ResolvedView {
view_type: view.view_type,
scenes,
transition: view.transition,
background: view_bg,
camera_easing: view.camera_easing,
camera_pan_duration: view.camera_pan_duration,
});
}
views
} else {
let mut scenes =
resolve_entries(scenario.scenes, source, 0, &mut audio, &mut included_paths)?;
for scene in &mut scenes {
resolve_scene_background(scene, templates)?;
}
vec![ResolvedView {
view_type: ViewType::Slide,
scenes,
transition: None,
background: ResolvedBackground::default(),
camera_easing: EasingType::EaseInOut,
camera_pan_duration: 0.8,
}]
};
Ok(ResolvedScenario {
video: scenario.video,
audio,
fonts: scenario.fonts,
views,
included_paths,
})
}
fn resolve_entries(
entries: Vec<SceneEntry>,
source: &IncludeSource,
depth: u8,
audio: &mut Vec<crate::schema::AudioTrack>,
included_paths: &mut Vec<PathBuf>,
) -> Result<Vec<Scene>> {
let mut result = Vec::new();
for entry in entries {
match entry {
SceneEntry::Scene(scene) => {
result.push(scene);
}
SceneEntry::Include(directive) => {
if depth >= MAX_INCLUDE_DEPTH {
return Err(RustmotionError::IncludeDepthExceeded {
limit: MAX_INCLUDE_DEPTH,
path: directive.include.clone(),
});
}
let scenes =
fetch_and_resolve(&directive, source, depth + 1, audio, included_paths)?;
result.extend(scenes);
}
}
}
Ok(result)
}
fn fetch_and_resolve(
directive: &IncludeDirective,
parent_source: &IncludeSource,
depth: u8,
audio: &mut Vec<crate::schema::AudioTrack>,
included_paths: &mut Vec<PathBuf>,
) -> Result<Vec<Scene>> {
let is_remote =
directive.include.starts_with("http://") || directive.include.starts_with("https://");
let (json_str, child_source) = if is_remote {
let body = fetch_remote(&directive.include)?;
let child_source = IncludeSource::File(PathBuf::from(&directive.include));
(body, child_source)
} else {
let path = resolve_local_path(&directive.include, parent_source)?;
let body =
std::fs::read_to_string(&path).map_err(|_| RustmotionError::IncludeFileNotFound {
path: path.display().to_string(),
})?;
included_paths.push(path.clone());
let child_source = IncludeSource::File(path);
(body, child_source)
};
let mut json_value: serde_json::Value =
serde_json::from_str(&json_str).map_err(RustmotionError::from)?;
crate::variables::apply_variables(
&mut json_value,
directive.config.as_ref(),
&directive.include,
)?;
if let IncludeSource::File(ref p) = child_source {
if let Some(dir) = p.parent() {
crate::assets::rebase_relative_paths(&mut json_value, dir);
}
}
crate::expand::expand_directives(&mut json_value, &directive.include)?;
let child_scenario: Scenario =
serde_json::from_value(json_value).map_err(RustmotionError::from)?;
audio.extend(child_scenario.audio);
let mut scenes = resolve_entries(
child_scenario.scenes,
&child_source,
depth,
audio,
included_paths,
)?;
if let Some(ref indices) = directive.scenes {
let total = scenes.len();
for &idx in indices {
if idx >= total {
return Err(RustmotionError::IncludeSceneOutOfBounds {
index: idx,
path: directive.include.clone(),
total,
});
}
}
let mut slots: Vec<Option<Scene>> = scenes.into_iter().map(Some).collect();
let mut filtered = Vec::with_capacity(indices.len());
for &idx in indices {
if let Some(scene) = slots[idx].take() {
filtered.push(scene);
}
}
scenes = filtered;
}
Ok(scenes)
}
fn resolve_local_path(relative: &str, source: &IncludeSource) -> Result<PathBuf> {
match source {
IncludeSource::File(parent_path) => {
let parent_dir = parent_path.parent().unwrap_or_else(|| Path::new("."));
Ok(parent_dir.join(relative))
}
IncludeSource::Inline => Err(RustmotionError::IncludeInlinePath {
path: relative.to_string(),
}),
}
}
fn fetch_remote(url: &str) -> Result<String> {
let response = ureq::get(url)
.call()
.map_err(|e| RustmotionError::IncludeRemoteFetch {
url: url.to_string(),
reason: e.to_string(),
})?;
let body =
response
.into_body()
.read_to_string()
.map_err(|e| RustmotionError::IncludeRemoteFetch {
url: url.to_string(),
reason: e.to_string(),
})?;
Ok(body)
}
use std::collections::HashMap;
fn resolve_entry(
entry: &BackgroundEntry,
templates: &HashMap<String, serde_json::Value>,
) -> Result<AnimatedBackground> {
let base = if let Some(ref name) = entry.template_ref {
let tmpl = templates
.get(name)
.ok_or_else(|| RustmotionError::UnknownBackgroundTemplate { name: name.clone() })?;
let mut base = tmpl.clone();
deep_merge(
&mut base,
&serde_json::Value::Object(entry.overrides.clone()),
);
base
} else {
serde_json::Value::Object(entry.overrides.clone())
};
let bg: AnimatedBackground = serde_json::from_value(base)?;
validate_animated_bg(&bg)?;
Ok(bg)
}
fn validate_animated_bg(bg: &AnimatedBackground) -> Result<()> {
if let BackgroundPreset::Heropattern(cfg) = &bg.preset {
if crate::engine::heropatterns::find_pattern(&cfg.pattern).is_none() {
return Err(RustmotionError::UnknownHeropattern {
name: cfg.pattern.clone(),
});
}
}
Ok(())
}
fn resolve_background_value(
bg_value: Option<&BackgroundValue>,
legacy: &[AnimatedBackground],
templates: &HashMap<String, serde_json::Value>,
) -> Result<ResolvedBackground> {
let mut resolved = ResolvedBackground::default();
if let Some(bg) = bg_value {
match bg {
BackgroundValue::Color(s) => {
resolved.color = Some(s.clone());
}
BackgroundValue::Single(entry) => {
resolved.animated.push(resolve_entry(entry, templates)?);
resolved.transition = entry.transition.clone();
}
BackgroundValue::Multiple(entries) => {
for entry in entries {
resolved.animated.push(resolve_entry(entry, templates)?);
if resolved.transition.is_none() {
resolved.transition = entry.transition.clone();
}
}
}
}
}
for bg in legacy {
validate_animated_bg(bg)?;
}
resolved.animated.extend_from_slice(legacy);
Ok(resolved)
}
fn resolve_scene_background(
scene: &mut Scene,
templates: &HashMap<String, serde_json::Value>,
) -> Result<()> {
scene.resolved_background = resolve_background_value(
scene.background.as_ref(),
&scene.animated_background,
templates,
)?;
Ok(())
}
fn deep_merge(base: &mut serde_json::Value, overlay: &serde_json::Value) {
if let (serde_json::Value::Object(b), serde_json::Value::Object(o)) = (base, overlay) {
for (k, v) in o {
if k == "$ref" || k == "transition" {
continue;
}
match b.get_mut(k) {
Some(existing) if existing.is_object() && v.is_object() => {
deep_merge(existing, v);
}
_ => {
b.insert(k.clone(), v.clone());
}
}
}
}
}