use std::fmt;
use std::path::{Path, PathBuf};
use serde::Deserialize;
use url::Url;
use crate::error::ConfigError;
use crate::relpath::reject_traversal;
#[derive(Clone, PartialEq, Eq, Deserialize)]
#[serde(try_from = "String")]
pub(crate) struct Secret(String);
impl Secret {
#[must_use]
pub(crate) fn expose(&self) -> &str {
&self.0
}
#[cfg(test)]
#[must_use]
pub(crate) fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[cfg(test)]
#[must_use]
pub(crate) fn is_blank(&self) -> bool {
self.0.trim().is_empty()
}
}
impl TryFrom<String> for Secret {
type Error = ConfigError;
fn try_from(value: String) -> Result<Secret, ConfigError> {
if value.trim().is_empty() {
return Err(ConfigError::parse("secret must not be empty"));
}
Ok(Secret(value))
}
}
impl TryFrom<&str> for Secret {
type Error = ConfigError;
fn try_from(value: &str) -> Result<Secret, ConfigError> {
Secret::try_from(value.to_string())
}
}
impl fmt::Debug for Secret {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Secret(redacted)")
}
}
impl fmt::Display for Secret {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("redacted")
}
}
#[derive(Clone, PartialEq, Eq, Deserialize)]
#[serde(try_from = "String")]
pub(crate) struct GatewayUrl(String);
impl GatewayUrl {
#[must_use]
pub(crate) fn as_str(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for GatewayUrl {
type Error = ConfigError;
fn try_from(value: String) -> Result<GatewayUrl, ConfigError> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(ConfigError::parse("[gateway].url must not be empty"));
}
let parsed = Url::parse(trimmed).map_err(|e| {
ConfigError::parse(format!(
"[gateway].url must be a valid http or https URL: {value:?}: {e}"
))
})?;
if !matches!(parsed.scheme(), "http" | "https") {
return Err(ConfigError::parse(format!(
"[gateway].url must be an http or https URL: {value:?}"
)));
}
if parsed.host_str().is_none_or(str::is_empty) {
return Err(ConfigError::parse(format!(
"[gateway].url must have a host: {value:?}"
)));
}
Ok(GatewayUrl(value))
}
}
impl fmt::Debug for GatewayUrl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("GatewayUrl").field(&self.0).finish()
}
}
#[derive(Clone, PartialEq, Eq, Deserialize)]
#[serde(try_from = "PathBuf")]
pub(crate) struct RelativePromptPath(PathBuf);
impl RelativePromptPath {
#[must_use]
pub(crate) fn as_path(&self) -> &Path {
&self.0
}
}
impl TryFrom<PathBuf> for RelativePromptPath {
type Error = ConfigError;
fn try_from(value: PathBuf) -> Result<RelativePromptPath, ConfigError> {
reject_traversal(&value).map_err(|detail| {
ConfigError::parse(format!("[prompts] file {}: {detail}", value.display()))
})?;
Ok(RelativePromptPath(value))
}
}
impl fmt::Debug for RelativePromptPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("RelativePromptPath").field(&self.0).finish()
}
}
#[derive(Clone, PartialEq, Eq, Deserialize)]
#[serde(try_from = "String")]
pub(crate) struct GlobPattern(String);
impl GlobPattern {
#[must_use]
pub(crate) fn as_str(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for GlobPattern {
type Error = ConfigError;
fn try_from(value: String) -> Result<GlobPattern, ConfigError> {
reject_traversal(Path::new(&value)).map_err(|detail| {
ConfigError::parse(format!("[catalog] pattern {value:?}: {detail}"))
})?;
glob::Pattern::new(&value)
.map_err(|e| ConfigError::parse(format!("[catalog] pattern {value:?}: {e}")))?;
Ok(GlobPattern(value))
}
}
impl fmt::Debug for GlobPattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("GlobPattern").field(&self.0).finish()
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize)]
#[serde(try_from = "String")]
pub(crate) struct PromptName(String);
const MAX_PROMPT_NAME_LEN: usize = 48;
const RESERVED_PROMPT_NAMES: [&str; 4] = ["list_prompts", "run_prompt", "check_run", "need_prompt"];
impl PromptName {
#[must_use]
pub(crate) fn as_str(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for PromptName {
type Error = ConfigError;
fn try_from(value: String) -> Result<PromptName, ConfigError> {
let mut chars = value.chars();
let well_formed = chars.next().is_some_and(|first| first.is_ascii_lowercase())
&& value.len() <= MAX_PROMPT_NAME_LEN
&& chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_');
if !well_formed {
return Err(ConfigError::parse(format!(
"[prompts.{value}] key is not ^[a-z][a-z0-9_]{{0,{}}}$",
MAX_PROMPT_NAME_LEN - 1
)));
}
if RESERVED_PROMPT_NAMES.contains(&value.as_str()) {
return Err(ConfigError::parse(format!(
"[prompts.{value}] key is reserved: a built-in tool already answers to it"
)));
}
Ok(PromptName(value))
}
}
impl std::borrow::Borrow<str> for PromptName {
fn borrow(&self) -> &str {
&self.0
}
}
impl fmt::Debug for PromptName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("PromptName").field(&self.0).finish()
}
}