1use crate::{
6 ManifestError,
7 system::{Arch, OperatingSystem, platform_arch, platform_os},
8 validation::{validate_color, validate_id, validate_name},
9};
10use garde::Validate;
11use indexmap::IndexMap;
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14use serde_with::skip_serializing_none;
15use std::{fmt::Display, str::FromStr};
16
17#[derive(
21 Debug, Clone, Serialize, Deserialize, Validate, Hash, PartialEq, Eq, PartialOrd, Ord, JsonSchema,
22)]
23#[garde(transparent)]
24#[serde(transparent)]
25pub struct PluginId(#[garde(custom(validate_id))] pub String);
26
27impl TryFrom<String> for PluginId {
28 type Error = garde::Report;
29 fn try_from(value: String) -> Result<Self, Self::Error> {
30 Self::from_str(&value)
31 }
32}
33
34impl FromStr for PluginId {
35 type Err = garde::Report;
36 fn from_str(s: &str) -> Result<Self, Self::Err> {
37 let value = PluginId(s.to_string());
38 value.validate()?;
39 Ok(value)
40 }
41}
42
43impl Display for PluginId {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 self.0.fmt(f)
46 }
47}
48
49impl PluginId {
50 pub fn as_str(&self) -> &str {
51 self.0.as_str()
52 }
53}
54
55impl AsRef<str> for PluginId {
56 fn as_ref(&self) -> &str {
57 self.as_str()
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
65#[serde(transparent)]
66#[schemars(with = "String", example = "=22.18.0")]
67pub struct BinaryNodeVersion(pub node_semver::Range);
68
69impl AsRef<node_semver::Range> for BinaryNodeVersion {
70 fn as_ref(&self) -> &node_semver::Range {
71 &self.0
72 }
73}
74
75#[skip_serializing_none]
77#[derive(Debug, Clone, Deserialize, Serialize, Validate, JsonSchema)]
78pub struct PluginManifest {
79 #[garde(dive)]
81 pub plugin: MPlugin,
82
83 #[garde(dive)]
86 pub bin: Option<MBin>,
87
88 #[garde(dive)]
90 pub category: MCategory,
91
92 #[garde(dive)]
94 pub actions: ActionMap,
95}
96
97impl TryFrom<&str> for PluginManifest {
98 type Error = ManifestError;
99
100 fn try_from(value: &str) -> Result<Self, Self::Error> {
101 let manifest: PluginManifest = serde_json::from_str(value)?;
102 manifest.validate()?;
103 Ok(manifest)
104 }
105}
106
107impl TryFrom<&[u8]> for PluginManifest {
108 type Error = ManifestError;
109 fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
110 let manifest: PluginManifest = serde_json::from_slice(value)?;
111 manifest.validate()?;
112 Ok(manifest)
113 }
114}
115
116impl PluginManifest {
117 #[inline]
119 pub fn parse(value: &str) -> Result<PluginManifest, ManifestError> {
120 Self::try_from(value)
121 }
122}
123
124#[skip_serializing_none]
126#[derive(Debug, Clone, Deserialize, Serialize, Validate, JsonSchema)]
127pub struct MPlugin {
128 #[garde(dive)]
130 #[schemars(example = "com.jacobtread.tilepad.obs")]
131 pub id: PluginId,
132 #[garde(length(min = 1))]
134 #[schemars(example = "Example Plugin")]
135 pub name: String,
136 #[garde(length(min = 1))]
138 #[schemars(example = "0.1.0")]
139 pub version: String,
140 #[garde(inner(length(min = 1)))]
142 #[schemars(example = ["Example Author 1", "Example Author 2"])]
143 pub authors: Vec<String>,
144 #[garde(skip)]
146 #[schemars(example = "My plugin that performs my actions")]
147 pub description: Option<String>,
148 #[garde(skip)]
150 #[schemars(example = "images/icon.svg")]
151 pub icon: Option<String>,
152 #[garde(skip)]
155 #[schemars(skip)]
156 pub internal: Option<bool>,
157}
158
159#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
164pub struct ActionMap(pub IndexMap<ActionId, ManifestAction>);
165
166impl AsRef<IndexMap<ActionId, ManifestAction>> for ActionMap {
167 fn as_ref(&self) -> &IndexMap<ActionId, ManifestAction> {
168 &self.0
169 }
170}
171
172#[skip_serializing_none]
174#[derive(Debug, Clone, Deserialize, Serialize, Validate, JsonSchema)]
175pub struct MCategory {
176 #[garde(length(min = 1))]
178 #[schemars(example = "My Category")]
179 pub label: String,
180 #[garde(skip)]
182 #[schemars(example = "images/icon.svg")]
183 pub icon: Option<String>,
184}
185
186#[derive(
190 Debug, Clone, Serialize, Deserialize, Validate, PartialEq, Eq, PartialOrd, Ord, Hash, JsonSchema,
191)]
192#[garde(transparent)]
193#[serde(transparent)]
194#[schemars(example = &"example_action")]
195pub struct ActionId(#[garde(custom(validate_name))] pub String);
196
197impl ActionId {
198 pub fn as_str(&self) -> &str {
199 self.0.as_str()
200 }
201}
202
203impl TryFrom<String> for ActionId {
204 type Error = garde::Report;
205 fn try_from(value: String) -> Result<Self, Self::Error> {
206 Self::from_str(&value)
207 }
208}
209
210impl FromStr for ActionId {
211 type Err = garde::Report;
212 fn from_str(s: &str) -> Result<Self, Self::Err> {
213 let value = ActionId(s.to_string());
214 value.validate()?;
215 Ok(value)
216 }
217}
218
219impl AsRef<str> for ActionId {
220 fn as_ref(&self) -> &str {
221 self.as_str()
222 }
223}
224
225impl Display for ActionId {
226 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227 self.0.fmt(f)
228 }
229}
230
231#[skip_serializing_none]
233#[derive(Debug, Clone, Deserialize, Serialize, Validate, JsonSchema)]
234pub struct ManifestAction {
235 #[garde(length(min = 1))]
237 #[schemars(example = "My Action")]
238 pub label: String,
239
240 #[garde(skip)]
243 #[schemars(example = "images/icon.svg")]
244 pub icon: Option<String>,
245
246 #[garde(skip)]
249 #[schemars(example = "display/my-display.display.html")]
250 pub display: Option<String>,
251
252 #[garde(dive)]
255 pub icon_options: Option<ManifestActionIconOptions>,
256
257 #[garde(skip)]
260 #[schemars(example = "My action")]
261 pub description: Option<String>,
262
263 #[garde(skip)]
265 #[schemars(example = "inspector/index.html")]
266 pub inspector: Option<String>,
267}
268
269#[skip_serializing_none]
271#[derive(Default, Debug, Clone, Serialize, Deserialize, Validate, JsonSchema)]
272#[serde(default)]
273pub struct ManifestActionIconOptions {
274 #[garde(skip)]
276 pub padding: Option<u32>,
277
278 #[garde(inner(custom(validate_color)))]
280 #[schemars(example = "#ffffff")]
281 pub background_color: Option<String>,
282
283 #[garde(inner(custom(validate_color)))]
285 #[schemars(example = "#ffffff")]
286 pub border_color: Option<String>,
287}
288
289#[derive(Debug, Clone, Deserialize, Serialize, Validate, JsonSchema)]
291#[serde(untagged)]
292pub enum MBin {
293 Node {
295 #[garde(dive)]
296 node: MBinNode,
297 },
298
299 Native {
301 #[garde(dive)]
302 native: Vec<MBinNative>,
303 },
304}
305
306#[derive(Debug, Clone, Deserialize, Serialize, Validate, JsonSchema)]
309pub struct MBinNode {
310 #[garde(length(min = 1))]
314 #[schemars(example = "bin/index.js")]
315 pub entrypoint: String,
316
317 #[garde(skip)]
323 #[serde(default = "default_node_version")]
324 pub version: BinaryNodeVersion,
325}
326
327fn default_node_version() -> BinaryNodeVersion {
329 BinaryNodeVersion(
330 node_semver::Range::parse("=22.18.0").expect("hardcoded range should be valid"),
331 )
332}
333
334#[derive(Debug, Clone, Deserialize, Serialize, Validate, JsonSchema)]
337pub struct MBinNative {
338 #[garde(skip)]
340 pub os: OperatingSystem,
341
342 #[garde(skip)]
344 pub arch: Arch,
345
346 #[garde(length(min = 1))]
348 #[schemars(example = "bin/example.exe")]
349 pub path: String,
350}
351
352impl MBinNative {
353 pub fn is_usable(&self, os: &OperatingSystem, arch: &Arch) -> bool {
355 self.os.eq(os) && self.arch.eq(arch)
356 }
357
358 pub fn find_usable<'a>(
360 options: &'a [MBinNative],
361 os: &OperatingSystem,
362 arch: &Arch,
363 ) -> Option<&'a Self> {
364 options.iter().find(|bin| bin.is_usable(os, arch))
365 }
366
367 pub fn find_current(options: &[MBinNative]) -> Option<&Self> {
369 let os = platform_os();
370 let arch = platform_arch();
371 Self::find_usable(options, &os, &arch)
372 }
373}
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 #[test]
379 fn test_is_usable_true_when_matches() {
380 let bin = MBinNative {
381 os: OperatingSystem::Linux,
382 arch: Arch::X64,
383 path: "bin/linux-x64".to_string(),
384 };
385 assert!(bin.is_usable(&OperatingSystem::Linux, &Arch::X64));
386 }
387
388 #[test]
389 fn test_is_usable_false_when_os_mismatch() {
390 let bin = MBinNative {
391 os: OperatingSystem::Linux,
392 arch: Arch::X64,
393 path: "bin/linux-x64".to_string(),
394 };
395 assert!(!bin.is_usable(&OperatingSystem::Windows, &Arch::X64));
396 }
397
398 #[test]
399 fn test_is_usable_false_when_arch_mismatch() {
400 let bin = MBinNative {
401 os: OperatingSystem::Linux,
402 arch: Arch::X64,
403 path: "bin/linux-x64".to_string(),
404 };
405 assert!(!bin.is_usable(&OperatingSystem::Linux, &Arch::X86));
406 }
407
408 #[test]
409 fn test_find_usable_finds_correct_bin() {
410 let bins = vec![
411 MBinNative {
412 os: OperatingSystem::Windows,
413 arch: Arch::X64,
414 path: "bin/win-x64".to_string(),
415 },
416 MBinNative {
417 os: OperatingSystem::Linux,
418 arch: Arch::X64,
419 path: "bin/linux-x64".to_string(),
420 },
421 ];
422 let result = MBinNative::find_usable(&bins, &OperatingSystem::Linux, &Arch::X64);
423 assert!(result.is_some());
424 assert_eq!(result.unwrap().path, "bin/linux-x64");
425 }
426
427 #[test]
428 fn test_find_usable_returns_none_if_no_match() {
429 let bins = vec![
430 MBinNative {
431 os: OperatingSystem::Windows,
432 arch: Arch::X64,
433 path: "bin/win-x64".to_string(),
434 },
435 MBinNative {
436 os: OperatingSystem::MacOs,
437 arch: Arch::Arm64,
438 path: "bin/macos-arm64".to_string(),
439 },
440 ];
441 let result = MBinNative::find_usable(&bins, &OperatingSystem::Linux, &Arch::X64);
442 assert!(result.is_none());
443 }
444
445 #[test]
446 fn test_find_usable_returns_first_match() {
447 let bins = vec![
448 MBinNative {
449 os: OperatingSystem::Linux,
450 arch: Arch::X64,
451 path: "bin/linux-x64-v1".to_string(),
452 },
453 MBinNative {
454 os: OperatingSystem::Linux,
455 arch: Arch::X64,
456 path: "bin/linux-x64-v2".to_string(),
457 },
458 ];
459 let result = MBinNative::find_usable(&bins, &OperatingSystem::Linux, &Arch::X64);
460 assert!(result.is_some());
461 assert_eq!(result.unwrap().path, "bin/linux-x64-v1");
462 }
463}