use std::{fmt, str::FromStr};
use mant_ir::{NodeId, OutlinePath, is_normalized_node_id};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::NodePath;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
pub enum ContentSelector {
Path {
#[schemars(length(min = 1, max = 512))]
path: NodePath,
},
Id {
#[schemars(length(min = 1, max = 512))]
id: NodeId,
},
}
impl<'de> Deserialize<'de> for ContentSelector {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
enum Wire {
Path { path: NodePath },
Id { id: NodeId },
}
let selector = match Wire::deserialize(deserializer)? {
Wire::Path { path } => Self::Path { path },
Wire::Id { id } => Self::Id { id },
};
selector.validate().map_err(serde::de::Error::custom)?;
Ok(selector)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InvalidContentSelector;
impl fmt::Display for InvalidContentSelector {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(
"use path:<canonical outline path> or id:<canonical content ID> (maximum 512 bytes)",
)
}
}
impl std::error::Error for InvalidContentSelector {}
impl ContentSelector {
#[must_use]
pub fn path(value: impl Into<NodePath>) -> Self {
Self::Path { path: value.into() }
}
#[must_use]
pub fn id(value: impl Into<NodeId>) -> Self {
Self::Id { id: value.into() }
}
#[must_use]
pub fn value(&self) -> &str {
match self {
Self::Path { path } => path.as_str(),
Self::Id { id } => id.as_str(),
}
}
pub fn validate(&self) -> Result<(), InvalidContentSelector> {
let value = self.value();
if value.is_empty() || value.len() > 512 {
return Err(InvalidContentSelector);
}
let valid = match self {
Self::Path { .. } => value
.parse::<OutlinePath>()
.is_ok_and(|path| path.to_string() == value),
Self::Id { .. } => is_normalized_node_id(value),
};
valid.then_some(()).ok_or(InvalidContentSelector)
}
}
impl FromStr for ContentSelector {
type Err = InvalidContentSelector;
fn from_str(value: &str) -> Result<Self, Self::Err> {
if value.len() > 517 {
return Err(InvalidContentSelector);
}
let selected = if let Some(id) = value.strip_prefix("id:") {
Self::id(id)
} else {
Self::path(value.strip_prefix("path:").unwrap_or(value))
};
selected.validate()?;
Ok(selected)
}
}
impl fmt::Display for ContentSelector {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Path { path } => write!(f, "path:{path}"),
Self::Id { id } => write!(f, "id:{id}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strict_namespaces_and_closed_wire_have_no_name_or_link_fallback() {
for value in ["root", "0", "1.2/e3", "path:1.2/e3", "id:option-x"] {
assert!(value.parse::<ContentSelector>().is_ok(), "{value}");
}
for value in [
"option-x",
"--help",
"#topic",
"https://example.com",
" 1 ",
"01",
"path:topic",
"id:Mixed.Target",
] {
assert!(value.parse::<ContentSelector>().is_err(), "{value}");
}
assert_ne!(ContentSelector::path("root"), ContentSelector::id("root"));
for value in [
r#""1""#,
r#"{"kind":"path","path":"1","future":true}"#,
r#"{"kind":"name","name":"--help"}"#,
] {
assert!(serde_json::from_str::<ContentSelector>(value).is_err());
}
for value in [
serde_json::json!({"kind":"path","path":""}),
serde_json::json!({"kind":"path","path":"01"}),
serde_json::json!({"kind":"id","id":"Mixed.Target"}),
serde_json::json!({"kind":"id","id":"x\n"}),
serde_json::json!({"kind":"id","id":"x".repeat(513)}),
] {
assert!(serde_json::from_value::<ContentSelector>(value).is_err());
}
let selected = ContentSelector::id("option-x");
assert_eq!(
serde_json::from_str::<ContentSelector>(&serde_json::to_string(&selected).unwrap())
.unwrap(),
selected
);
assert!(ContentSelector::path("1.".repeat(1000)).validate().is_err());
}
}