mant_protocol/
content_selector.rs1use std::{fmt, str::FromStr};
4
5use mant_ir::{NodeId, OutlinePath, is_normalized_node_id};
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8
9use crate::NodePath;
10
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, JsonSchema)]
16#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
17pub enum ContentSelector {
18 Path {
20 #[schemars(length(min = 1, max = 512))]
22 path: NodePath,
23 },
24 Id {
26 #[schemars(length(min = 1, max = 512))]
28 id: NodeId,
29 },
30}
31
32impl<'de> Deserialize<'de> for ContentSelector {
33 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
34 #[derive(Deserialize)]
35 #[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
36 enum Wire {
37 Path { path: NodePath },
38 Id { id: NodeId },
39 }
40 let selector = match Wire::deserialize(deserializer)? {
41 Wire::Path { path } => Self::Path { path },
42 Wire::Id { id } => Self::Id { id },
43 };
44 selector.validate().map_err(serde::de::Error::custom)?;
45 Ok(selector)
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct InvalidContentSelector;
52
53impl fmt::Display for InvalidContentSelector {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 f.write_str(
56 "use path:<canonical outline path> or id:<canonical content ID> (maximum 512 bytes)",
57 )
58 }
59}
60
61impl std::error::Error for InvalidContentSelector {}
62
63impl ContentSelector {
64 #[must_use]
67 pub fn path(value: impl Into<NodePath>) -> Self {
68 Self::Path { path: value.into() }
69 }
70
71 #[must_use]
73 pub fn id(value: impl Into<NodeId>) -> Self {
74 Self::Id { id: value.into() }
75 }
76
77 #[must_use]
79 pub fn value(&self) -> &str {
80 match self {
81 Self::Path { path } => path.as_str(),
82 Self::Id { id } => id.as_str(),
83 }
84 }
85
86 pub fn validate(&self) -> Result<(), InvalidContentSelector> {
91 let value = self.value();
92 if value.is_empty() || value.len() > 512 {
93 return Err(InvalidContentSelector);
94 }
95 let valid = match self {
96 Self::Path { .. } => value
97 .parse::<OutlinePath>()
98 .is_ok_and(|path| path.to_string() == value),
99 Self::Id { .. } => is_normalized_node_id(value),
100 };
101 valid.then_some(()).ok_or(InvalidContentSelector)
102 }
103}
104
105impl FromStr for ContentSelector {
106 type Err = InvalidContentSelector;
107
108 fn from_str(value: &str) -> Result<Self, Self::Err> {
109 if value.len() > 517 {
111 return Err(InvalidContentSelector);
112 }
113 let selected = if let Some(id) = value.strip_prefix("id:") {
114 Self::id(id)
115 } else {
116 Self::path(value.strip_prefix("path:").unwrap_or(value))
117 };
118 selected.validate()?;
119 Ok(selected)
120 }
121}
122
123impl fmt::Display for ContentSelector {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 match self {
126 Self::Path { path } => write!(f, "path:{path}"),
127 Self::Id { id } => write!(f, "id:{id}"),
128 }
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn strict_namespaces_and_closed_wire_have_no_name_or_link_fallback() {
138 for value in ["root", "0", "1.2/e3", "path:1.2/e3", "id:option-x"] {
139 assert!(value.parse::<ContentSelector>().is_ok(), "{value}");
140 }
141 for value in [
142 "option-x",
143 "--help",
144 "#topic",
145 "https://example.com",
146 " 1 ",
147 "01",
148 "path:topic",
149 "id:Mixed.Target",
150 ] {
151 assert!(value.parse::<ContentSelector>().is_err(), "{value}");
152 }
153 assert_ne!(ContentSelector::path("root"), ContentSelector::id("root"));
154 for value in [
155 r#""1""#,
156 r#"{"kind":"path","path":"1","future":true}"#,
157 r#"{"kind":"name","name":"--help"}"#,
158 ] {
159 assert!(serde_json::from_str::<ContentSelector>(value).is_err());
160 }
161 for value in [
162 serde_json::json!({"kind":"path","path":""}),
163 serde_json::json!({"kind":"path","path":"01"}),
164 serde_json::json!({"kind":"id","id":"Mixed.Target"}),
165 serde_json::json!({"kind":"id","id":"x\n"}),
166 serde_json::json!({"kind":"id","id":"x".repeat(513)}),
167 ] {
168 assert!(serde_json::from_value::<ContentSelector>(value).is_err());
169 }
170 let selected = ContentSelector::id("option-x");
171 assert_eq!(
172 serde_json::from_str::<ContentSelector>(&serde_json::to_string(&selected).unwrap())
173 .unwrap(),
174 selected
175 );
176 assert!(ContentSelector::path("1.".repeat(1000)).validate().is_err());
177 }
178}