1use object_store::path::DELIMITER;
17use std::{future::Future, pin::Pin};
18
19pub mod agent;
20pub mod context;
21pub mod http;
22pub mod json;
23pub mod model;
24pub mod tool;
25
26pub use agent::*;
27pub use context::*;
28pub use http::*;
29pub use json::*;
30pub use model::*;
31pub use tool::*;
32
33pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
36
37pub type BoxPinFut<T> = Pin<Box<dyn Future<Output = T> + Send>>;
39
40pub type BoxFut<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
42
43pub fn path_lowercase(path: &Path) -> Path {
50 let mut raw = path.to_string();
51 raw.make_ascii_lowercase();
52 Path::parse(&raw).unwrap_or_else(|_| Path::from(raw))
55}
56
57pub fn path_join(a: &Path, b: &Path) -> Path {
66 path_lowercase(&Path::from_iter(a.parts().chain(b.parts())))
67}
68
69pub fn validate_path_part(part: &str) -> Result<(), BoxError> {
74 if part.is_empty() || part.contains(DELIMITER) || Path::from(part).as_ref() != part {
75 return Err(format!("invalid path part: {}", part).into());
76 }
77
78 Ok(())
79}
80
81pub fn validate_function_name(name: &str) -> Result<(), BoxError> {
89 if name.is_empty() {
90 return Err("empty string".into());
91 }
92
93 if name.len() > 64 {
94 return Err("string length exceeds the limit 64".into());
95 }
96
97 let mut iter = name.chars();
98 if !matches!(iter.next(), Some('a'..='z')) {
99 return Err("name must start with a lowercase letter".into());
100 }
101
102 for c in iter {
103 if !matches!(c, 'a'..='z' | '0'..='9' | '_' | '-') {
104 return Err(format!("invalid character: {}", c).into());
105 }
106 }
107 Ok(())
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn test_path_lowercase() {
116 let a = Path::from("a/Foo");
117 assert_eq!(path_lowercase(&a).as_ref(), "a/foo");
118 }
119
120 #[test]
121 fn test_path_join_handles_root() {
122 let root = Path::default();
123 let p = Path::from("a/b");
124 assert_eq!(path_join(&root, &p).as_ref(), "a/b");
125 assert_eq!(path_join(&p, &root).as_ref(), "a/b");
126 assert_eq!(path_join(&root, &root).as_ref(), "");
127 assert_eq!(path_join(&p, &Path::from("c")).as_ref(), "a/b/c");
128 }
129
130 #[test]
131 fn test_path_join_is_idempotent_and_does_not_double_encode() {
132 let ns = Path::from("agents/store");
133 let key = Path::from("a*b");
135 let joined = path_join(&ns, &key);
136
137 assert!(!joined.as_ref().contains("%25"));
139
140 let rejoined = path_join(&Path::default(), &joined);
143 assert_eq!(joined, rejoined);
144 assert_eq!(path_lowercase(&joined), joined);
145 }
146
147 #[test]
148 fn test_validate_path_part() {
149 assert!(validate_path_part("foo").is_ok());
150 assert!(validate_path_part("fOO").is_ok());
151 assert!(validate_path_part("").is_err());
152 assert!(validate_path_part("foo/").is_err());
153 assert!(validate_path_part("/foo").is_err());
154 assert!(validate_path_part("foo/bar").is_err());
155 assert!(validate_path_part("foo/bar/").is_err());
156 }
157
158 #[test]
159 fn test_validate_function_name() {
160 assert!(validate_function_name("foo").is_ok());
161 assert!(validate_function_name("foo_bar9").is_ok());
162 assert!(validate_function_name("foo-bar").is_ok());
163
164 assert!(validate_function_name("").is_err());
165 assert!(validate_function_name("9foo").is_err());
166 assert!(validate_function_name("foo.bar").is_err());
167 assert!(validate_function_name("foo/bar").is_err());
168 assert!(validate_function_name(&"a".repeat(65)).is_err());
169 }
170}