1use std::fmt::Write as _;
2
3use thiserror::Error;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum ModelTransport {
11 OpenAiResponses { base_url_template: &'static str },
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, Error)]
16pub enum TemplateError {
17 #[error("template variable '{name}' could not be resolved in '{template}'")]
18 UnresolvedVariable { name: String, template: String },
19 #[error("template '{template}' has an unterminated '${{' placeholder")]
20 UnterminatedPlaceholder { template: String },
21}
22
23pub fn expand_api_template(template: &str, lookup: impl Fn(&str) -> Option<String>) -> Result<String, TemplateError> {
29 let mut out = String::with_capacity(template.len());
30 let mut rest = template;
31
32 while let Some(start) = rest.find("${") {
33 out.push_str(&rest[..start]);
34 let after = &rest[start + 2..];
35 let Some(end) = after.find('}') else {
36 return Err(TemplateError::UnterminatedPlaceholder { template: template.to_string() });
37 };
38 let name = &after[..end];
39 let value = lookup(name).ok_or_else(|| TemplateError::UnresolvedVariable {
40 name: name.to_string(),
41 template: template.to_string(),
42 })?;
43 let _ = out.write_str(&value);
44 rest = &after[end + 1..];
45 }
46
47 out.push_str(rest);
48 Ok(out)
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54
55 fn lookup<'a>(vars: &'a [(&'a str, &'a str)]) -> impl Fn(&str) -> Option<String> + 'a {
56 move |name| vars.iter().find(|(k, _)| *k == name).map(|(_, v)| (*v).to_string())
57 }
58
59 #[test]
60 fn expands_a_single_variable() {
61 let expanded = expand_api_template(
62 "https://bedrock-mantle.${AWS_REGION}.api.aws/openai/v1",
63 lookup(&[("AWS_REGION", "us-west-2")]),
64 )
65 .unwrap();
66
67 assert_eq!(expanded, "https://bedrock-mantle.us-west-2.api.aws/openai/v1");
68 }
69
70 #[test]
71 fn leaves_templates_without_placeholders_untouched() {
72 let expanded = expand_api_template("https://bedrock-mantle.us-east-1.api.aws/v1", lookup(&[])).unwrap();
73
74 assert_eq!(expanded, "https://bedrock-mantle.us-east-1.api.aws/v1");
75 }
76
77 #[test]
78 fn expands_multiple_variables() {
79 let expanded = expand_api_template(
80 "https://${HOST}/v1/projects/${PROJECT}/end",
81 lookup(&[("HOST", "example.com"), ("PROJECT", "p-1")]),
82 )
83 .unwrap();
84
85 assert_eq!(expanded, "https://example.com/v1/projects/p-1/end");
86 }
87
88 #[test]
89 fn unresolved_variable_is_an_error() {
90 let error = expand_api_template("https://x.${AWS_REGION}.aws/v1", lookup(&[])).unwrap_err();
91
92 assert_eq!(
93 error,
94 TemplateError::UnresolvedVariable {
95 name: "AWS_REGION".to_string(),
96 template: "https://x.${AWS_REGION}.aws/v1".to_string(),
97 }
98 );
99 }
100
101 #[test]
102 fn unterminated_placeholder_is_an_error() {
103 let error = expand_api_template("https://x.${AWS_REGION/v1", lookup(&[])).unwrap_err();
104
105 assert!(matches!(error, TemplateError::UnterminatedPlaceholder { .. }));
106 }
107}