use thiserror::Error;
pub type Result<T> = std::result::Result<T, RustmotionError>;
#[derive(Debug, Error)]
pub enum RustmotionError {
#[error("{0}")]
Generic(String),
#[error("Failed to read '{path}': {source}")]
FileRead {
path: String,
source: std::io::Error,
},
#[error("Failed to parse JSON: {source}")]
JsonParse {
#[from]
source: serde_json::Error,
},
#[error("HTML transpile error: {0}")]
HtmlParse(String),
#[error("Failed to load image '{path}': {reason}")]
ImageLoad { path: String, reason: String },
#[error("Failed to decode image '{path}'")]
ImageDecode { path: String },
#[error("SVG component must have either 'src' or 'data'")]
SvgMissingSrc,
#[error("Failed to load SVG '{path}': {reason}")]
SvgLoad { path: String, reason: String },
#[error("Failed to parse SVG: {reason}")]
SvgParse { reason: String },
#[error("Failed to create pixmap for {target}")]
PixmapCreation { target: String },
#[error("Invalid icon format: '{icon}' (expected 'prefix:name')")]
InvalidIconFormat { icon: String },
#[error("Failed to fetch icon '{icon}': {reason}")]
IconFetch { icon: String, reason: String },
#[error("Failed to parse icon SVG '{icon}': {reason}")]
IconParse { icon: String, reason: String },
#[error("Failed to create Skia image from {target}")]
SkiaImageCreation { target: String },
#[error("Failed to open GIF '{path}': {reason}")]
GifOpen { path: String, reason: String },
#[error("Failed to decode GIF '{path}': {reason}")]
GifDecode { path: String, reason: String },
#[error("QR code generation failed: {reason}")]
QrCodeGeneration { reason: String },
#[error("No fonts available on this system")]
FontNotFound,
#[error(
"FontEntry for '{family}' has source=\"google\" but also sets 'path' — use one or the other"
)]
FontSourceAndPathConflict { family: String },
#[error(
"FontEntry for '{family}' requires either 'path' (local file) or 'source' (e.g. \"google\")"
)]
FontMissingPath { family: String },
#[error(
"Google Fonts: failed to fetch CSS for '{family}' (url: {url}): {reason}\n\
Tip: if you are offline, manually place a TTF for each weight at:\n {cache_hint}"
)]
GoogleFontsFetch {
family: String,
url: String,
reason: String,
cache_hint: String,
},
#[error(
"Google Fonts: no font URLs found in CSS response for '{family}' — \
the family name may be misspelled or unavailable"
)]
GoogleFontsNoUrls { family: String },
#[error("Google Fonts: failed to download TTF for '{family}' weight {weight}: {reason}")]
GoogleFontsTtfFetch {
family: String,
weight: u16,
reason: String,
},
#[error("Include depth limit ({limit}) exceeded while resolving '{path}'")]
IncludeDepthExceeded { limit: u8, path: String },
#[error("Include: scenes[{index}] is out of bounds in '{path}' (file has {total} scenes)")]
IncludeSceneOutOfBounds {
index: usize,
path: String,
total: usize,
},
#[error("Include: cannot resolve relative path '{path}' from inline JSON (use a file path or URL instead)")]
IncludeInlinePath { path: String },
#[error("Include: failed to fetch '{url}': {reason}")]
IncludeRemoteFetch { url: String, reason: String },
#[error("Include: file not found '{path}'")]
IncludeFileNotFound { path: String },
#[error(
"Scenario cannot have both top-level 'scenes' and 'composition' — use one or the other"
)]
CompositionAndScenesConflict,
#[error("Unknown background template '{name}' referenced via $ref")]
UnknownBackgroundTemplate { name: String },
#[error("Unknown heropattern '{name}' — see heropatterns.com for available patterns")]
UnknownHeropattern { name: String },
#[error("Variable '${name}' is not defined in '{path}'")]
UndefinedVariable { name: String, path: String },
#[error("Variable '{name}' in '{path}' is missing a default value")]
VariableMissingDefault { name: String, path: String },
#[error("Unresolved variable reference '${name}' after substitution in '{path}'")]
UnresolvedVariable { name: String, path: String },
#[error("Cannot interpolate non-string variable '${name}' into string in '{path}'")]
VariableInterpolationTypeError { name: String, path: String },
#[error("'components' at '{path}' must be an object mapping names to definitions")]
ComponentsBlockNotObject { path: String },
#[error("Component definition '{name}' at '{path}' is invalid: {reason}")]
ComponentDefinitionInvalid {
name: String,
path: String,
reason: String,
},
#[error("'use' directive at '{path}' is invalid: {reason}")]
UseDirectiveInvalid { path: String, reason: String },
#[error(
"Unknown component '{name}' referenced via 'use' at '{path}' — no such name in this \
file's 'components' block"
)]
UnknownComponent { name: String, path: String },
#[error(
"Missing required parameter '{param}' for component '{component}' at '{path}' — it has \
no default and was not supplied via 'props'"
)]
ComponentParamMissing {
component: String,
param: String,
path: String,
},
#[error(
"Unknown parameter '{param}' passed to component '{component}' at '{path}' — not \
declared in its 'params'"
)]
UnknownComponentParam {
component: String,
param: String,
path: String,
},
#[error("Component instantiation cycle at '{path}': {chain}")]
ComponentCycle { chain: String, path: String },
#[error("'for-each' directive at '{path}' is invalid: {reason}")]
ForEachDirectiveInvalid { path: String, reason: String },
#[error("'for-each' at '{path}' must resolve to an array; found {found}")]
ForEachNotArray { path: String, found: String },
#[error(
"Template/component expansion depth limit ({limit}) exceeded at '{path}' — likely a \
runaway nested 'use'/'for-each' template"
)]
ExpansionDepthExceeded { limit: u32, path: String },
#[error("No frames to render (total duration is 0)")]
NoFrames,
#[error("codec '{codec}' cannot be written into a .{container} file — {fix}")]
CodecContainerMismatch {
codec: String,
container: String,
fix: String,
},
#[error("Failed to run ffmpeg: {reason}. Is ffmpeg installed?")]
FfmpegSpawn { reason: String },
#[error("FFmpeg encoding failed{}", .stderr.as_ref().map(|s| format!(": {}", s)).unwrap_or_default())]
FfmpegFailed { stderr: Option<String> },
#[error("Failed to open FFmpeg stdin pipe")]
FfmpegPipe,
#[error("Failed to write to FFmpeg pipe: {reason}{}", .stderr.as_ref().map(|s| format!("\nffmpeg reported:\n{}", s)).unwrap_or_default())]
FfmpegWrite {
reason: String,
stderr: Option<String>,
},
#[error("Failed to wait for FFmpeg: {reason}")]
FfmpegWait { reason: String },
#[error("ffmpeg failed to extract frame from '{src}'")]
FfmpegFrameExtract { src: String },
#[error("Failed to create GIF encoder: {reason}")]
GifEncoder { reason: String },
#[error("Failed to set GIF repeat: {reason}")]
GifRepeat { reason: String },
#[error("Failed to write GIF frame: {reason}")]
GifFrame { reason: String },
#[error("Failed to open audio file '{path}': {reason}")]
AudioOpen { path: String, reason: String },
#[error("Failed to probe audio format for '{path}': {reason}")]
AudioProbe { path: String, reason: String },
#[error("No audio track found in '{path}'")]
AudioNoTrack { path: String },
#[error("Failed to create decoder for '{path}': {reason}")]
AudioDecoder { path: String, reason: String },
#[error("Failed to create Skia surface")]
SurfaceCreation,
#[error("Failed to create image from pixels")]
PixelImage,
#[error("Failed to read pixels from Skia surface")]
PixelRead,
#[error("Failed to create motion blur surface")]
MotionBlurSurface,
#[error("Cannot use both input file and --json")]
ConflictingInput,
#[error("Provide either an input file or --json")]
MissingInput,
#[error("--watch requires an input file path (cannot use --json or stdin)")]
WatchRequiresFile,
#[error("Frame {frame} is out of range (total frames: {total})")]
FrameOutOfRange { frame: u32, total: u32 },
#[error("Frame range {start}-{end} is out of range (total frames: {total})")]
FrameRangeOutOfRange { start: u32, end: u32, total: u32 },
#[error("Time {time:.2}s is beyond video duration")]
TimeOutOfRange { time: f64 },
#[error("File watcher channel closed")]
WatcherClosed,
#[error("Validation failed: {schema_errors} schema error(s), {geometry_violations} geometry violation(s), {unresolved_vars} unresolved variable(s). Run `rustmotion validate -f <file>` to see details.")]
ValidationFailed {
schema_errors: usize,
geometry_violations: usize,
unresolved_vars: usize,
},
#[error("Incremental encoding unsupported: {reason}")]
IncrementalUnsupported { reason: String },
#[error("Invalid CRF value {value}: must be between 0 and 51")]
InvalidCrf { value: u8 },
#[error("Unknown codec '{codec}'. Supported: h264, h265, vp9, prores")]
UnknownCodec { codec: String },
#[error("Path is not valid UTF-8: '{path}'")]
NonUtf8Path { path: String },
#[error("Failed to create preview window: {reason}")]
PreviewWindow { reason: String },
#[error("Failed to read Lottie file '{path}': {reason}")]
LottieRead { path: String, reason: String },
#[error("Lottie component requires either 'src' or 'data'")]
LottieMissingSrc,
#[error("Failed to read Lottie frame '{path}': {reason}")]
LottieFrameRead { path: String, reason: String },
#[error("Failed to decode Lottie frame '{path}': {reason}")]
LottieFrameDecode { path: String, reason: String },
#[error("Lottie render failed: {reason}")]
LottieRender { reason: String },
#[error(
"Unknown skill or rule: '{name}'. Run `rustmotion skills list` to see available rules."
)]
UnknownSkill { name: String },
#[error("{0}")]
Io(#[from] std::io::Error),
#[error("Image processing error: {0}")]
Image(#[from] image::ImageError),
#[error("File watcher error: {0}")]
Notify(#[from] notify::Error),
#[error("{0}")]
Other(String),
}
impl From<String> for RustmotionError {
fn from(s: String) -> Self {
RustmotionError::Other(s)
}
}