Skip to main content

amethystate_core/
scheme.rs

1use crate::path::StorePath;
2use std::fmt::Write;
3
4/// The Tauri event a frontend hears changes at `path` on.
5pub fn event_channel(path: &StorePath) -> String {
6    let mut name = String::from("amethystate://");
7    for (at, level) in path.segments().enumerate() {
8        if at > 0 {
9            name.push(':');
10        }
11        for byte in level.as_str().bytes() {
12            match byte {
13                b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' => name.push(byte as char),
14                other => {
15                    let _ = write!(name, "_{other:02x}");
16                }
17            }
18        }
19    }
20    name
21}
22
23#[derive(Debug, Clone, Copy)]
24pub enum FieldKind {
25    Plain,
26    /// A struct of its own, whose entry says what it holds.
27    Nested {
28        entry: &'static SchemaExportEntry,
29    },
30    Volatile,
31    ReactiveMap {
32        key_type: &'static str,
33        value_type: &'static str,
34        key_rust_type: &'static str,
35        value_rust_type: &'static str,
36        /// Whether a level under the map names a key of its key type.
37        accepts_key: fn(&str) -> bool,
38    },
39}
40
41#[derive(Debug, Clone, Copy)]
42pub struct FieldExportMeta {
43    pub name: &'static str,
44    /// Where the field is stored under its holder, its levels joined by dots:
45    /// the name after `rename_all` or `path`, and empty for a flattened node.
46    pub stored: &'static str,
47    pub ts_type: &'static str,
48    pub full_ts_type: &'static str,
49    pub rust_type: &'static str,
50    pub kind: FieldKind,
51    /// Whether a JSON text is a value this field holds; for a map, a value of
52    /// one of its entries.
53    pub accepts: fn(&str) -> bool,
54    /// The JSON its declared default is stored as; `None` for anything but a
55    /// plain field.
56    pub default: fn() -> Option<String>,
57    /// Whether it takes its default again once its key is gone, where the
58    /// field said; `None` leaves it to its struct.
59    pub resets: Option<bool>,
60}
61
62#[derive(Debug, Clone, Copy)]
63pub struct SchemaExportEntry {
64    pub prefix: Option<&'static str>,
65    pub struct_name: &'static str,
66    /// The module the struct is declared in, which tells apart two of one name.
67    pub module_path: &'static str,
68    /// The line the struct is a version of, where it named one.
69    pub id: Option<&'static str>,
70    pub version: u32,
71    /// Whether its fields take their default again once a key is gone, where
72    /// the struct said; `None` leaves it to the struct holding it, and at the
73    /// top to the store.
74    pub resets: Option<bool>,
75    pub fields: &'static [FieldExportMeta],
76}
77
78impl SchemaExportEntry {
79    /// Whether `other` was made from the same declaration.
80    pub fn is(&self, other: &SchemaExportEntry) -> bool {
81        self.module_path == other.module_path && self.struct_name == other.struct_name
82    }
83}
84
85/// What a struct tells a Tauri frontend about itself.
86///
87/// Implemented by `#[amethystate]` for every struct when the `tauri` feature is
88/// on; a struct holding another reaches it through this rather than by name.
89pub trait Exported {
90    const EXPORT: SchemaExportEntry;
91}
92
93inventory::collect!(SchemaExportEntry);
94
95/// Every struct with a place of its own, at the newest version of its line.
96///
97/// A line kept beside its older versions for a migration is one struct here:
98/// the older ones describe places the store no longer has. It is fixed for the
99/// life of the process, so it is walked on the first ask and handed out as a
100/// slice afterwards.
101pub fn exports() -> &'static [&'static SchemaExportEntry] {
102    static COMPILED: std::sync::OnceLock<Vec<&'static SchemaExportEntry>> =
103        std::sync::OnceLock::new();
104
105    COMPILED.get_or_init(|| {
106        let mut newest: Vec<&'static SchemaExportEntry> = Vec::new();
107        for entry in inventory::iter::<SchemaExportEntry> {
108            let line = |other: &&mut &'static SchemaExportEntry| match entry.id {
109                Some(id) => other.id == Some(id),
110                None => other.id.is_none() && other.prefix == entry.prefix,
111            };
112            match newest.iter_mut().find(line) {
113                Some(kept) if kept.version < entry.version => *kept = entry,
114                Some(kept) if kept.version == entry.version && !kept.is(entry) => {
115                    newest.push(entry)
116                }
117                Some(_) => {}
118                None => newest.push(entry),
119            }
120        }
121        newest
122    })
123}