Skip to main content

wenlan_types/
space_context.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use serde::de::{Error as _, Visitor};
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use std::fmt;
6
7/// Caller intent for a top-level write destination.
8///
9/// The wire shape stays ergonomic: an omitted field inherits context,
10/// explicit JSON `null` selects Uncategorized, and a string names a Space.
11#[derive(Debug, Clone, PartialEq, Eq, Default)]
12pub enum WriteSpaceTarget {
13    #[default]
14    Inherit,
15    Uncategorized,
16    Named(String),
17}
18
19impl WriteSpaceTarget {
20    pub fn is_inherit(&self) -> bool {
21        matches!(self, Self::Inherit)
22    }
23
24    pub fn named(&self) -> Option<&str> {
25        match self {
26            Self::Named(name) => Some(name),
27            Self::Inherit | Self::Uncategorized => None,
28        }
29    }
30
31    pub fn as_deref(&self) -> Option<&str> {
32        self.named()
33    }
34}
35
36impl From<Option<String>> for WriteSpaceTarget {
37    fn from(value: Option<String>) -> Self {
38        match value {
39            Some(name) => Self::Named(name),
40            None => Self::Inherit,
41        }
42    }
43}
44
45impl From<String> for WriteSpaceTarget {
46    fn from(value: String) -> Self {
47        Self::Named(value)
48    }
49}
50
51impl Serialize for WriteSpaceTarget {
52    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
53    where
54        S: Serializer,
55    {
56        match self {
57            Self::Inherit | Self::Uncategorized => serializer.serialize_none(),
58            Self::Named(name) => serializer.serialize_str(name),
59        }
60    }
61}
62
63impl<'de> Deserialize<'de> for WriteSpaceTarget {
64    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
65    where
66        D: Deserializer<'de>,
67    {
68        struct TargetVisitor;
69
70        impl<'de> Visitor<'de> for TargetVisitor {
71            type Value = WriteSpaceTarget;
72
73            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
74                formatter.write_str("null or a non-empty Space name")
75            }
76
77            fn visit_none<E>(self) -> Result<Self::Value, E>
78            where
79                E: serde::de::Error,
80            {
81                Ok(WriteSpaceTarget::Uncategorized)
82            }
83
84            fn visit_unit<E>(self) -> Result<Self::Value, E>
85            where
86                E: serde::de::Error,
87            {
88                Ok(WriteSpaceTarget::Uncategorized)
89            }
90
91            fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
92            where
93                D: Deserializer<'de>,
94            {
95                let value = String::deserialize(deserializer)?;
96                let trimmed = value.trim();
97                if trimmed.is_empty() {
98                    return Err(D::Error::custom("Space name must not be empty"));
99                }
100                Ok(WriteSpaceTarget::Named(trimmed.to_string()))
101            }
102        }
103
104        deserializer.deserialize_option(TargetVisitor)
105    }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(rename_all = "snake_case")]
110pub enum WriteSpaceSource {
111    Request,
112    Header,
113    Default,
114    Uncategorized,
115    Existing,
116    #[serde(other)]
117    Unknown,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
121#[serde(rename_all = "snake_case")]
122pub enum WriteOutcome {
123    Created,
124    ResolvedExisting,
125    AttachedExisting,
126    #[serde(other)]
127    Unknown,
128}
129
130#[cfg(test)]
131mod tests {
132    use super::{WriteOutcome, WriteSpaceSource, WriteSpaceTarget};
133    use serde::{Deserialize, Serialize};
134
135    #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
136    struct WriteFixture {
137        #[serde(default, skip_serializing_if = "WriteSpaceTarget::is_inherit")]
138        space: WriteSpaceTarget,
139    }
140
141    #[test]
142    fn write_target_distinguishes_missing_null_and_named_space() {
143        let missing: WriteFixture = serde_json::from_str("{}").unwrap();
144        let uncategorized: WriteFixture = serde_json::from_str(r#"{"space":null}"#).unwrap();
145        let named: WriteFixture = serde_json::from_str(r#"{"space":"Work"}"#).unwrap();
146
147        assert_eq!(missing.space, WriteSpaceTarget::Inherit);
148        assert_eq!(uncategorized.space, WriteSpaceTarget::Uncategorized);
149        assert_eq!(named.space, WriteSpaceTarget::Named("Work".to_string()));
150        assert_eq!(serde_json::to_string(&missing).unwrap(), "{}");
151        assert_eq!(
152            serde_json::to_string(&uncategorized).unwrap(),
153            r#"{"space":null}"#
154        );
155        assert_eq!(
156            serde_json::to_string(&named).unwrap(),
157            r#"{"space":"Work"}"#
158        );
159    }
160
161    #[test]
162    fn receipt_enums_accept_future_values() {
163        assert_eq!(
164            serde_json::from_str::<WriteSpaceSource>(r#""future_source""#).unwrap(),
165            WriteSpaceSource::Unknown
166        );
167        assert_eq!(
168            serde_json::from_str::<WriteOutcome>(r#""future_outcome""#).unwrap(),
169            WriteOutcome::Unknown
170        );
171    }
172}