zng_view_api/
api_extension.rs

1//! API extension types.
2
3use std::{fmt, ops};
4
5use serde::{Deserialize, Serialize};
6use zng_txt::Txt;
7
8/// Custom serialized data, in a format defined by the extension.
9///
10/// Note that the bytes here should represent a serialized small `struct` only, you
11/// can add an [`IpcBytes`] field to this struct to transfer large payloads.
12///
13/// [`IpcBytes`]: zng_task::channel::IpcBytes
14#[derive(Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
15pub struct ApiExtensionPayload(#[serde(with = "serde_bytes")] pub Vec<u8>);
16impl ApiExtensionPayload {
17    /// Serialize the payload.
18    pub fn serialize<T: Serialize>(payload: &T) -> Result<Self, bincode::error::EncodeError> {
19        bincode::serde::encode_to_vec(payload, bincode::config::standard()).map(Self)
20    }
21
22    /// Deserialize the payload.
23    pub fn deserialize<T: serde::de::DeserializeOwned>(&self) -> Result<T, ApiExtensionRecvError> {
24        if let Some((id, error)) = self.parse_invalid_request() {
25            Err(ApiExtensionRecvError::InvalidRequest {
26                extension_id: id,
27                error: Txt::from_str(error),
28            })
29        } else if let Some(id) = self.parse_unknown_extension() {
30            Err(ApiExtensionRecvError::UnknownExtension { extension_id: id })
31        } else {
32            match bincode::serde::decode_from_slice(&self.0, bincode::config::standard()) {
33                Ok((r, _)) => Ok(r),
34                Err(e) => Err(ApiExtensionRecvError::Deserialize(e)),
35            }
36        }
37    }
38
39    /// Empty payload.
40    pub const fn empty() -> Self {
41        Self(vec![])
42    }
43
44    /// Value returned when an invalid extension is requested.
45    ///
46    /// Value is a string `"zng-view-api.unknown_extension;id={extension_id}"`.
47    pub fn unknown_extension(extension_id: ApiExtensionId) -> Self {
48        Self(format!("zng-view-api.unknown_extension;id={extension_id}").into_bytes())
49    }
50
51    /// Value returned when an invalid request is made for a valid extension key.
52    ///
53    /// Value is a string `"zng-view-api.invalid_request;id={extension_id};error={error}"`.
54    pub fn invalid_request(extension_id: ApiExtensionId, error: impl fmt::Display) -> Self {
55        Self(format!("zng-view-api.invalid_request;id={extension_id};error={error}").into_bytes())
56    }
57
58    /// If the payload is an [`unknown_extension`] error message, returns the key.
59    ///
60    /// if the payload starts with the invalid request header and the key cannot be retrieved the
61    /// [`ApiExtensionId::INVALID`] is returned as the key.
62    ///
63    /// [`unknown_extension`]: Self::unknown_extension
64    pub fn parse_unknown_extension(&self) -> Option<ApiExtensionId> {
65        let p = self.0.strip_prefix(b"zng-view-api.unknown_extension;")?;
66        if let Some(p) = p.strip_prefix(b"id=")
67            && let Ok(id_str) = std::str::from_utf8(p)
68        {
69            return match id_str.parse::<ApiExtensionId>() {
70                Ok(id) => Some(id),
71                Err(id) => Some(id),
72            };
73        }
74        Some(ApiExtensionId::INVALID)
75    }
76
77    /// If the payload is an [`invalid_request`] error message, returns the key and error.
78    ///
79    /// if the payload starts with the invalid request header and the key cannot be retrieved the
80    /// [`ApiExtensionId::INVALID`] is returned as the key and the error message will mention "corrupted payload".
81    ///
82    /// [`invalid_request`]: Self::invalid_request
83    pub fn parse_invalid_request(&self) -> Option<(ApiExtensionId, &str)> {
84        let p = self.0.strip_prefix(b"zng-view-api.invalid_request;")?;
85        if let Some(p) = p.strip_prefix(b"id=")
86            && let Some(id_end) = p.iter().position(|&b| b == b';')
87            && let Ok(id_str) = std::str::from_utf8(&p[..id_end])
88        {
89            let id = match id_str.parse::<ApiExtensionId>() {
90                Ok(id) => id,
91                Err(id) => id,
92            };
93            if let Some(p) = p[id_end..].strip_prefix(b";error=")
94                && let Ok(err_str) = std::str::from_utf8(p)
95            {
96                return Some((id, err_str));
97            }
98            return Some((id, "invalid request, corrupted payload, unknown error"));
99        }
100        Some((
101            ApiExtensionId::INVALID,
102            "invalid request, corrupted payload, unknown extension_id and error",
103        ))
104    }
105}
106impl fmt::Debug for ApiExtensionPayload {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        write!(f, "ExtensionPayload({} bytes)", self.0.len())
109    }
110}
111
112/// Identifies an API extension and version.
113///
114/// Note that the version is part of the name, usually in the pattern "crate-name.extension.v2",
115/// there are no minor versions, all different versions are considered breaking changes and
116/// must be announced and supported by exact match only. You can still communicate non-breaking changes
117/// by using the extension payload
118#[derive(Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
119pub struct ApiExtensionName {
120    name: Txt,
121}
122impl ApiExtensionName {
123    /// New from unique name.
124    ///
125    /// The name must contain at least 1 characters, and match the pattern `[a-zA-Z][a-zA-Z0-9-_.]`.
126    pub fn new(name: impl Into<Txt>) -> Result<Self, ApiExtensionNameError> {
127        let name = name.into();
128        Self::new_impl(name)
129    }
130    fn new_impl(name: Txt) -> Result<ApiExtensionName, ApiExtensionNameError> {
131        if name.is_empty() {
132            return Err(ApiExtensionNameError::NameCannotBeEmpty);
133        }
134        for (i, c) in name.char_indices() {
135            if i == 0 {
136                if !c.is_ascii_alphabetic() {
137                    return Err(ApiExtensionNameError::NameCannotStartWithChar(c));
138                }
139            } else if !c.is_ascii_alphanumeric() && c != '_' && c != '-' && c != '.' {
140                return Err(ApiExtensionNameError::NameInvalidChar(c));
141            }
142        }
143
144        Ok(Self { name })
145    }
146}
147impl fmt::Debug for ApiExtensionName {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        fmt::Debug::fmt(&self.name, f)
150    }
151}
152impl fmt::Display for ApiExtensionName {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        fmt::Display::fmt(&self.name, f)
155    }
156}
157impl ops::Deref for ApiExtensionName {
158    type Target = str;
159
160    fn deref(&self) -> &Self::Target {
161        self.name.as_str()
162    }
163}
164impl From<&'static str> for ApiExtensionName {
165    fn from(value: &'static str) -> Self {
166        Self::new(value).unwrap()
167    }
168}
169
170/// API extension invalid name.
171#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
172#[non_exhaustive]
173pub enum ApiExtensionNameError {
174    /// Name cannot empty `""`.
175    NameCannotBeEmpty,
176    /// Name can only start with ASCII alphabetic chars `[a-zA-Z]`.
177    NameCannotStartWithChar(char),
178    /// Name can only contains `[a-zA-Z0-9-_.]`.
179    NameInvalidChar(char),
180}
181impl fmt::Display for ApiExtensionNameError {
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        match self {
184            ApiExtensionNameError::NameCannotBeEmpty => write!(f, "API extension name cannot be empty"),
185            ApiExtensionNameError::NameCannotStartWithChar(c) => {
186                write!(f, "API cannot start with '{c}', name pattern `[a-zA-Z][a-zA-Z0-9-_.]`")
187            }
188            ApiExtensionNameError::NameInvalidChar(c) => write!(f, "API cannot contain '{c}', name pattern `[a-zA-Z][a-zA-Z0-9-_.]`"),
189        }
190    }
191}
192impl std::error::Error for ApiExtensionNameError {}
193
194/// List of available API extensions.
195#[derive(Default, Clone, Debug, serde::Serialize, serde::Deserialize)]
196pub struct ApiExtensions(Vec<ApiExtensionName>);
197impl ops::Deref for ApiExtensions {
198    type Target = [ApiExtensionName];
199
200    fn deref(&self) -> &Self::Target {
201        &self.0
202    }
203}
204impl ApiExtensions {
205    /// New Empty.
206    pub fn new() -> Self {
207        Self::default()
208    }
209
210    /// Gets the position of the `ext` in the list of available extensions. This index
211    /// identifies the API extension in the [`Api::app_extension`] and [`Api::render_extension`].
212    ///
213    /// The key can be cached only for the duration of the view process, each view re-instantiation
214    /// must query for the presence of the API extension again, and it may change position on the list.
215    ///
216    /// [`Api::app_extension`]: crate::Api::app_extension
217    /// [`Api::render_extension`]: crate::Api::render_extension
218    pub fn id(&self, ext: &ApiExtensionName) -> Option<ApiExtensionId> {
219        self.0.iter().position(|e| e == ext).map(ApiExtensionId::from_index)
220    }
221
222    /// Push the `ext` to the list, if it is not already inserted.
223    ///
224    /// Returns `Ok(key)` if inserted or `Err(key)` is was already in list.
225    pub fn insert(&mut self, ext: ApiExtensionName) -> Result<ApiExtensionId, ApiExtensionId> {
226        if let Some(key) = self.id(&ext) {
227            Err(key)
228        } else {
229            let key = self.0.len();
230            self.0.push(ext);
231            Ok(ApiExtensionId::from_index(key))
232        }
233    }
234}
235
236/// Identifies an [`ApiExtensionName`] in a list.
237#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
238#[serde(transparent)]
239pub struct ApiExtensionId(u32);
240impl fmt::Debug for ApiExtensionId {
241    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242        if *self == Self::INVALID {
243            if f.alternate() {
244                write!(f, "ApiExtensionId::")?;
245            }
246            write!(f, "INVALID")
247        } else {
248            write!(f, "ApiExtensionId({})", self.0 - 1)
249        }
250    }
251}
252impl fmt::Display for ApiExtensionId {
253    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254        if *self == Self::INVALID {
255            write!(f, "invalid")
256        } else {
257            write!(f, "{}", self.0 - 1)
258        }
259    }
260}
261impl ApiExtensionId {
262    /// Dummy ID.
263    pub const INVALID: Self = Self(0);
264
265    /// Gets the ID as a list index.
266    ///
267    /// # Panics
268    ///
269    /// Panics if called in `INVALID`.
270    pub fn index(self) -> usize {
271        self.0.checked_sub(1).expect("invalid id") as _
272    }
273
274    /// New ID from the index of an [`ApiExtensionName`] in a list.
275    ///
276    /// # Panics
277    ///
278    /// Panics if `idx > u32::MAX - 1`.
279    pub fn from_index(idx: usize) -> Self {
280        if idx > (u32::MAX - 1) as _ {
281            panic!("index out-of-bounds")
282        }
283        Self(idx as u32 + 1)
284    }
285}
286impl std::str::FromStr for ApiExtensionId {
287    type Err = Self;
288
289    fn from_str(s: &str) -> Result<Self, Self::Err> {
290        match s.parse::<u32>() {
291            Ok(i) => {
292                let r = Self::from_index(i as _);
293                if r == Self::INVALID { Err(r) } else { Ok(r) }
294            }
295            Err(_) => Err(Self::INVALID),
296        }
297    }
298}
299
300/// Error in the response of an API extension call.
301#[derive(Debug)]
302#[non_exhaustive]
303pub enum ApiExtensionRecvError {
304    /// Requested extension was not in the list of extensions.
305    UnknownExtension {
306        /// Extension that was requested.
307        ///
308        /// Is `INVALID` only if error message is corrupted.
309        extension_id: ApiExtensionId,
310    },
311    /// Invalid request format.
312    InvalidRequest {
313        /// Extension that was requested.
314        ///
315        /// Is `INVALID` only if error message is corrupted.
316        extension_id: ApiExtensionId,
317        /// Message from the view-process.
318        error: Txt,
319    },
320    /// Failed to deserialize to the expected response type.
321    Deserialize(bincode::error::DecodeError),
322}
323impl fmt::Display for ApiExtensionRecvError {
324    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
325        match self {
326            ApiExtensionRecvError::UnknownExtension { extension_id } => write!(f, "invalid API request for unknown id {extension_id:?}"),
327            ApiExtensionRecvError::InvalidRequest { extension_id, error } => {
328                write!(f, "invalid API request for extension id {extension_id:?}, {error}")
329            }
330            ApiExtensionRecvError::Deserialize(e) => write!(f, "API extension response failed to deserialize, {e}"),
331        }
332    }
333}
334impl std::error::Error for ApiExtensionRecvError {
335    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
336        if let Self::Deserialize(e) = self { Some(e) } else { None }
337    }
338}