systemprompt_models/documents/
uri.rs1use std::fmt;
8use std::str::FromStr;
9
10use serde::{Deserialize, Serialize};
11use systemprompt_identifiers::error::IdValidationError;
12use systemprompt_identifiers::{DocumentId, DocumentNodeId, DocumentRevisionId};
13
14#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
15pub enum DocumentUriError {
16 #[error("document URI must start with `document://`")]
17 MissingScheme,
18 #[error("document URI has an invalid {part}")]
19 InvalidPart {
20 part: &'static str,
21 #[source]
22 source: IdValidationError,
23 },
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
27#[serde(try_from = "String", into = "String")]
28pub struct DocumentUri {
29 pub document_id: DocumentId,
30 pub revision_id: Option<DocumentRevisionId>,
31 pub node: Option<DocumentNodeId>,
32}
33
34impl DocumentUri {
35 pub const SCHEME: &'static str = "document://";
36
37 #[must_use]
38 pub const fn new(document_id: DocumentId) -> Self {
39 Self {
40 document_id,
41 revision_id: None,
42 node: None,
43 }
44 }
45
46 #[must_use]
47 pub fn at_revision(mut self, revision_id: DocumentRevisionId) -> Self {
48 self.revision_id = Some(revision_id);
49 self
50 }
51
52 #[must_use]
53 pub fn at_node(mut self, node: DocumentNodeId) -> Self {
54 self.node = Some(node);
55 self
56 }
57}
58
59impl fmt::Display for DocumentUri {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 write!(f, "{}{}", Self::SCHEME, self.document_id.as_str())?;
62 if let Some(revision) = &self.revision_id {
63 write!(f, "@{}", revision.as_str())?;
64 }
65 if let Some(node) = &self.node {
66 write!(f, "#{}", node.as_str())?;
67 }
68 Ok(())
69 }
70}
71
72impl FromStr for DocumentUri {
73 type Err = DocumentUriError;
74
75 fn from_str(s: &str) -> Result<Self, Self::Err> {
76 let rest = s
77 .strip_prefix(Self::SCHEME)
78 .ok_or(DocumentUriError::MissingScheme)?;
79 let (rest, node) = match rest.split_once('#') {
80 Some((head, node)) => (head, Some(node)),
81 None => (rest, None),
82 };
83 let (document, revision) = match rest.split_once('@') {
84 Some((head, revision)) => (head, Some(revision)),
85 None => (rest, None),
86 };
87 let invalid = |part| move |source| DocumentUriError::InvalidPart { part, source };
88 Ok(Self {
89 document_id: DocumentId::try_new(document).map_err(invalid("document id"))?,
90 revision_id: revision
91 .map(DocumentRevisionId::try_new)
92 .transpose()
93 .map_err(invalid("revision id"))?,
94 node: node
95 .map(DocumentNodeId::try_new)
96 .transpose()
97 .map_err(invalid("node id"))?,
98 })
99 }
100}
101
102impl TryFrom<String> for DocumentUri {
103 type Error = DocumentUriError;
104
105 fn try_from(value: String) -> Result<Self, Self::Error> {
106 value.parse()
107 }
108}
109
110impl From<DocumentUri> for String {
111 fn from(uri: DocumentUri) -> Self {
112 uri.to_string()
113 }
114}
115
116impl schemars::JsonSchema for DocumentUri {
117 fn schema_name() -> std::borrow::Cow<'static, str> {
118 "DocumentUri".into()
119 }
120
121 fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
122 schemars::json_schema!({
123 "type": "string",
124 "pattern": "^document://[^@#]+(@[^#]+)?(#.+)?$"
125 })
126 }
127}