hyperion/effects/
definition.rs1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6use tokio::fs;
7use tracing::error;
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10#[serde(deny_unknown_fields)]
11pub struct EffectDefinition {
12 pub name: String,
14 #[serde(skip)]
16 pub file: PathBuf,
17 pub script: String,
19 #[serde(default)]
21 pub args: serde_json::Value,
22 #[serde(skip)]
24 base_path: Arc<PathBuf>,
25}
26
27#[derive(Debug, Error)]
28pub enum EffectDefinitionError {
29 #[error(transparent)]
31 Io(#[from] std::io::Error),
32 #[error("invalid effect Definitionification path")]
34 InvalidPath,
35 #[error(transparent)]
37 Json(#[from] serde_json::Error),
38}
39
40impl EffectDefinition {
41 pub async fn read_dir(path: impl AsRef<Path>) -> Result<Vec<Self>, EffectDefinitionError> {
42 let base_path = Arc::new(path.as_ref().to_owned());
43 let mut definitions = Vec::new();
44
45 let mut read_dir = fs::read_dir(path.as_ref()).await?;
46 loop {
47 let result = read_dir.next_entry().await;
48 match result {
49 Ok(None) => {
50 break;
51 }
52 Ok(Some(entry)) => {
53 let path = entry.path();
54 if path.extension().and_then(std::ffi::OsStr::to_str) != Some("json") {
55 continue;
56 }
57
58 match Self::from_file(&path, base_path.clone()).await {
59 Ok(definition) => {
60 definitions.push(definition);
61 }
62 Err(err) => {
63 error!(path = %path.display(), error = %err, "error reading effect definition");
64 }
65 }
66 }
67 Err(err) => {
68 error!(error = %err, "error reading effect definition directory");
69 }
70 }
71 }
72
73 Ok(definitions)
74 }
75
76 pub async fn read_file(path: impl AsRef<Path>) -> Result<Self, EffectDefinitionError> {
77 let path = path.as_ref();
78
79 Self::from_file(
80 path,
81 path.parent()
82 .ok_or(EffectDefinitionError::InvalidPath)?
83 .to_owned()
84 .into(),
85 )
86 .await
87 }
88
89 async fn from_file(
90 path: &Path,
91 base_path: Arc<PathBuf>,
92 ) -> Result<Self, EffectDefinitionError> {
93 let json = fs::read(path).await?;
95
96 let mut this: Self = serde_json::from_slice(&json)?;
98
99 this.file = path
101 .strip_prefix(&*base_path)
102 .map(|path| path.to_owned())
103 .unwrap_or_else(|_| path.to_owned());
104
105 this.base_path = base_path;
107
108 Ok(this)
109 }
110
111 pub fn script_path(&self) -> Result<PathBuf, EffectDefinitionError> {
112 let mut result = (*self.base_path).clone();
113 let subpath = PathBuf::from(&self.script);
114
115 for component in subpath.components() {
117 match component {
118 std::path::Component::CurDir => {
119 }
121 std::path::Component::Normal(component) => {
122 result.push(component);
123 }
124 _ => {
125 return Err(EffectDefinitionError::InvalidPath);
126 }
127 }
128 }
129
130 Ok(result)
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[tokio::test]
139 async fn load_builtin_effects() {
140 let paths = crate::global::Paths::new(None).expect("failed to load paths");
141 let effects_root = paths.resolve_path("$SYSTEM/effects");
142
143 let json_files = std::fs::read_dir(&effects_root)
145 .expect("effects directory not found")
146 .filter(|res| {
147 res.as_ref()
148 .map(|entry| {
149 entry.path().extension().and_then(std::ffi::OsStr::to_str) == Some("json")
150 })
151 .unwrap_or(false)
152 })
153 .count();
154
155 let parsed = EffectDefinition::read_dir(&effects_root)
156 .await
157 .expect("read_dir failed");
158
159 eprintln!("{:#?}", parsed);
160 assert_eq!(parsed.len(), json_files);
161 }
162}