covey_plugin/
manifest.rs

1use std::fmt;
2
3#[doc(hidden)]
4/// Private implementation details. Do not use.
5pub mod __private_generation {
6    pub use covey_manifest_macros::include_manifest;
7    pub use serde;
8    pub use serde_json;
9}
10
11#[macro_export]
12macro_rules! include_manifest {
13    ($path:literal) => {
14        $crate::manifest::__private_generation::include_manifest!(
15            file = $path,
16            // this can't be $crate as it's used by serde, requires a proper path
17            serde_path = covey_plugin::manifest::__private_generation::serde,
18            covey_plugin_path = $crate,
19        );
20
21        impl $crate::manifest::ManifestDeserialization for self::Config {
22            fn try_from_input(s: &str) -> Result<Self, $crate::manifest::DeserializationError> {
23                $crate::manifest::__private_generation::serde_json::from_str(s)
24                    .map_err(|e| $crate::manifest::DeserializationError(e.to_string()))
25            }
26        }
27    };
28    () => {
29        $crate::include_manifest!("./manifest.toml");
30    };
31}
32
33/// Automatically implemented by [`include_manifest!`].
34///
35/// The unit type `()` also implements this, ignoring the input.
36pub trait ManifestDeserialization: Sized {
37    /// Constructs `Self` from the user's plugin configuration.
38    ///
39    /// The input string is currently in JSON format. This may change
40    /// in the future.
41    fn try_from_input(s: &str) -> Result<Self, DeserializationError>;
42}
43
44impl ManifestDeserialization for () {
45    /// Ignores the input string and always succeeds.
46    fn try_from_input(_: &str) -> Result<Self, DeserializationError> {
47        Ok(())
48    }
49}
50
51/// Error obtained from deserializing the user's plugin configuration.
52#[derive(Debug, Clone)]
53pub struct DeserializationError(pub String);
54
55impl fmt::Display for DeserializationError {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        f.write_str("failed to deserialize plugin configuration: ")?;
58        f.write_str(&self.0)?;
59        Ok(())
60    }
61}
62
63impl std::error::Error for DeserializationError {}
64
65#[cfg(test)]
66mod tests {
67    use std::collections::HashMap;
68
69    mod config {
70        use crate::manifest::__private_generation;
71
72        __private_generation::include_manifest!(
73            serde_path = crate::manifest::__private_generation::serde,
74            covey_plugin_path = crate,
75            inline = r#"
76                name = "Open"
77                description = "Open URLs with a query"
78                repository = "https://github.com/blorbb/covey-plugins"
79                authors = ["blorbb"]
80
81                [[schema]]
82                id = "urls"
83                title = "List of URLs to show"
84                type.map.value-type.struct.fields = { name = "text", url = "text", extra-field = "int" }
85
86                [[schema]]
87                id = "thing-with-dash"
88                title = "Some selection"
89                type.selection.allowed-values = ["a", "ab", "ab-cde"]
90
91                [[schema]]
92                id = "with-default"
93                title = "Yet another selection"
94                type.selection.allowed-values = ["oaiwrha", "iosdg"]
95                type.selection.default = "iosdg"
96            "#
97        );
98    }
99
100    #[test]
101    fn expanded_types_exist() {
102        config::Config {
103            urls: HashMap::from([(
104                "key".to_string(),
105                config::urls::UrlsValue {
106                    name: "name".to_string(),
107                    url: "urls".to_string(),
108                    extra_field: 10,
109                },
110            )]),
111            thing_with_dash: config::thing_with_dash::ThingWithDashSelection::AbCde,
112            with_default: config::with_default::WithDefaultSelection::default(),
113        };
114
115        use config::CommandExt;
116        crate::ListItem::new("ajwroiajw").on_activate(async |menu| {
117            menu.close();
118            Ok(())
119        });
120    }
121
122    #[test]
123    fn deserialize_impls() {
124        let input = serde_json::json!({
125            "urls": {
126                "crates": {
127                    "name": "crates docs",
128                    "url": "docs.rs",
129                    "extra-field": 500
130                }
131            },
132            "thing-with-dash": "ab-cde",
133        });
134
135        let deserialized: config::Config = serde_json::from_str(&input.to_string()).unwrap();
136        assert_eq!(
137            deserialized,
138            config::Config {
139                urls: HashMap::from([(
140                    "crates".to_string(),
141                    config::urls::UrlsValue {
142                        name: "crates docs".to_string(),
143                        url: "docs.rs".to_string(),
144                        extra_field: 500,
145                    }
146                )]),
147                thing_with_dash: config::thing_with_dash::ThingWithDashSelection::AbCde,
148                with_default: config::with_default::WithDefaultSelection::Iosdg,
149            }
150        )
151    }
152}