amethystate/observability/introspect.rs
1//! Walking a declared struct's fields at runtime.
2//!
3//! [`InspectorBackend`](crate::store::InspectorBackend) asks the store what is
4//! on disk, from outside the program that wrote it. This asks the other side,
5//! from inside: what the declaration says, and what the struct is holding right
6//! now.
7//!
8//! Nothing here is typed. A caller that knows the type reads the field; this is
9//! for the callers that do not - a screen that draws a row per setting, a dump
10//! in a bug report, a terminal that lists what an application has.
11
12use std::fmt;
13
14use crate::migration::fields::Role;
15use amethystate_core::path::StorePath;
16
17/// One field, as somebody looking at it sees it.
18pub struct FieldView<'a> {
19 /// The name in the source: what `state.font_size` is called in Rust.
20 pub declared: &'static str,
21
22 /// The name it is stored under, which is `declared` unless `path` or
23 /// `rename_all` said otherwise. A dot in it is a level.
24 pub stored: &'static str,
25
26 /// Where it is, whole. What a person editing the file is looking at.
27 pub at: StorePath,
28
29 /// The type it was declared as, written the way it was written.
30 pub type_name: &'static str,
31
32 /// The doc comment on the field, empty where there was none.
33 ///
34 /// **A description, not a label.** It is in whatever language it was
35 /// written in and fixed when the crate was compiled, so it belongs in a
36 /// dump, a developer's terminal or generated documentation - not in a
37 /// shipped interface. What a settings screen wants is a string from its
38 /// own catalogue, and what it should look that string up by is
39 /// [`at`](FieldView::at): the stored path survives a Rust rename, and
40 /// changes only when the data moves, which is a migration either way.
41 pub described: &'static str,
42
43 pub role: Role,
44
45 /// Whether it is kept in memory and never stored.
46 pub volatile: bool,
47
48 /// What it holds, rendered: the value for a leaf, how many entries for a
49 /// map, and `<opaque>` where the type cannot be printed at all.
50 ///
51 /// Rendered rather than borrowed because a field hands its value back by
52 /// value - there is no reference to lend - and this is for showing.
53 pub shown: String,
54
55 /// What the store and this field disagree about, if anything.
56 pub disagreement: Option<Disagreement>,
57
58 /// The fields under this one, for a `nested` field.
59 pub inside: Option<&'a dyn Inspect>,
60}
61
62/// What a field and the store do not agree about.
63///
64/// The value is not what the field reports: either the store holds something
65/// the field cannot read, or a declared rule refused it. Either way the field
66/// is reporting its default or the last thing it held, and the stored bytes are
67/// where they were - so it can be looked at, and fixed.
68#[derive(Debug, Clone)]
69pub struct Disagreement {
70 pub at: StorePath,
71 pub reason: Reason,
72}
73
74/// `non_exhaustive` for the same reason [`StorageError`] is: it is a list of
75/// what the store can be found disagreeing about, it grows, and it is read
76/// rather than dispatched on.
77///
78/// [`StorageError`]: crate::store::StorageError
79#[derive(Debug, Clone)]
80#[non_exhaustive]
81pub enum Reason {
82 /// What is stored will not read back as the declared type.
83 WillNotRead(std::sync::Arc<str>),
84
85 /// It read, and a declared rule refused it.
86 Refused(std::sync::Arc<str>),
87
88 /// The field never wrote its default, because the store already held
89 /// something at its path that seeding would have destroyed - so the two
90 /// have disagreed from the first moment the field existed.
91 Occupied(std::sync::Arc<str>),
92
93 /// The store has let go of its file, so what the field holds is the last
94 /// thing it heard rather than what is on disk.
95 Closed,
96
97 /// A declared rule put a stored value right, and the store would not take
98 /// the correction back: the field holds what the rule made of it, the store
99 /// what it was given.
100 NotWrittenBack(std::sync::Arc<str>),
101}
102
103impl fmt::Display for Reason {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 match self {
106 Reason::WillNotRead(why) => write!(f, "will not read: {why}"),
107 Reason::Refused(why) => write!(f, "refused: {why}"),
108 Reason::Occupied(why) => write!(f, "already held something else: {why}"),
109 Reason::Closed => f.write_str("the store was closed, so this is the last it heard"),
110 Reason::NotWrittenBack(why) => {
111 write!(
112 f,
113 "put right by its rule, and the store would not take it back: {why}"
114 )
115 }
116 }
117 }
118}
119
120impl fmt::Display for Disagreement {
121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 let Self { at, reason } = self;
123 write!(f, "{at} {reason}")
124 }
125}
126
127impl std::error::Error for Disagreement {}
128
129/// A struct that can list its own fields.
130///
131/// Implemented by `#[amethystate]` for every struct it generates.
132pub trait Inspect {
133 /// Where this struct's fields hang.
134 fn at(&self) -> StorePath;
135
136 /// How many fields were declared.
137 fn field_count(&self) -> usize;
138
139 /// One of them, by position. `None` past the end.
140 fn field_at(&self, index: usize) -> Option<FieldView<'_>>;
141
142 /// All of them, in the order they were declared.
143 fn fields(&self) -> Fields<'_>
144 where
145 Self: Sized,
146 {
147 Fields {
148 of: self,
149 next: 0,
150 left: self.field_count(),
151 }
152 }
153
154 /// One of them by its name in the source.
155 fn field(&self, declared: &str) -> Option<FieldView<'_>>
156 where
157 Self: Sized,
158 {
159 (0..self.field_count())
160 .filter_map(|i| self.field_at(i))
161 .find(|view| view.declared == declared)
162 }
163}
164
165pub struct Fields<'a> {
166 of: &'a dyn Inspect,
167 next: usize,
168 left: usize,
169}
170
171impl<'a> Iterator for Fields<'a> {
172 type Item = FieldView<'a>;
173
174 fn next(&mut self) -> Option<Self::Item> {
175 let view = self.of.field_at(self.next)?;
176 self.next += 1;
177 self.left -= 1;
178 Some(view)
179 }
180
181 fn size_hint(&self) -> (usize, Option<usize>) {
182 (self.left, Some(self.left))
183 }
184}
185
186impl ExactSizeIterator for Fields<'_> {}