use std::fmt;
use serde_json::{Map, Value};
pub trait IntoCapability {
fn into_capability(self) -> CapabilitySpec;
}
#[derive(Clone, PartialEq)]
pub struct CapabilityRef {
id: String,
config: Value,
}
impl fmt::Debug for CapabilityRef {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CapabilityRef")
.field("id", &self.id)
.field("config", &"<redacted>")
.finish()
}
}
impl CapabilityRef {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
config: Value::Object(Map::new()),
}
}
pub fn config(mut self, config: impl Into<Value>) -> Self {
self.config = config.into();
self
}
pub fn id(&self) -> &str {
&self.id
}
pub fn config_value(&self) -> &Value {
&self.config
}
}
#[derive(Clone, Debug)]
pub struct CapabilitySpec {
reference: CapabilityRef,
#[cfg(feature = "capabilities")]
definition: Option<crate::capability::Definition>,
}
impl CapabilitySpec {
pub fn reference(reference: CapabilityRef) -> Self {
Self {
reference,
#[cfg(feature = "capabilities")]
definition: None,
}
}
#[cfg(feature = "capabilities")]
pub fn definition(definition: crate::capability::Definition) -> Self {
Self {
reference: CapabilityRef::new(definition.id()),
definition: Some(definition),
}
}
pub fn capability_ref(&self) -> &CapabilityRef {
&self.reference
}
pub(crate) fn into_parts(self) -> CapabilitySpecParts {
CapabilitySpecParts {
reference: self.reference,
#[cfg(feature = "capabilities")]
definition: self.definition,
}
}
}
pub(crate) struct CapabilitySpecParts {
pub reference: CapabilityRef,
#[cfg(feature = "capabilities")]
pub definition: Option<crate::capability::Definition>,
}
impl From<CapabilityRef> for CapabilitySpec {
fn from(reference: CapabilityRef) -> Self {
Self::reference(reference)
}
}
impl IntoCapability for CapabilitySpec {
fn into_capability(self) -> CapabilitySpec {
self
}
}
impl IntoCapability for CapabilityRef {
fn into_capability(self) -> CapabilitySpec {
self.into()
}
}
impl IntoCapability for &str {
fn into_capability(self) -> CapabilitySpec {
CapabilityRef::new(self).into()
}
}
impl IntoCapability for String {
fn into_capability(self) -> CapabilitySpec {
CapabilityRef::new(self).into()
}
}
impl IntoCapability for &String {
fn into_capability(self) -> CapabilitySpec {
CapabilityRef::new(self.as_str()).into()
}
}
impl IntoCapability for everruns_core::AgentCapabilityConfig {
fn into_capability(self) -> CapabilitySpec {
CapabilityRef::new(self.capability_id())
.config(self.config)
.into()
}
}
#[cfg(feature = "capabilities")]
impl From<crate::capability::Definition> for CapabilitySpec {
fn from(definition: crate::capability::Definition) -> Self {
Self::definition(definition)
}
}
#[cfg(feature = "capabilities")]
impl IntoCapability for crate::capability::Definition {
fn into_capability(self) -> CapabilitySpec {
self.into()
}
}
pub(crate) fn validate_capability_id(id: &str) -> Result<(), String> {
if id.is_empty() {
return Err("capability id must not be empty".to_string());
}
if id.len() > 128 {
return Err(format!(
"capability id must be at most 128 bytes (got {})",
id.len()
));
}
if id.starts_with("__everruns_") {
return Err("capability id uses the reserved '__everruns_' namespace".to_string());
}
let mut chars = id.chars();
let first = chars.next().expect("non-empty checked above");
if !(first.is_ascii_alphabetic() || first == '_') {
return Err("capability id must start with a letter or underscore".to_string());
}
if id
.chars()
.any(|ch| !(ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.' | ':')))
{
return Err(
"capability id may only contain ASCII letters, digits, '_', '-', '.' or ':'"
.to_string(),
);
}
Ok(())
}
pub(crate) fn validate_capability_config(config: &Value) -> Result<(), String> {
if config.is_object() {
Ok(())
} else {
Err("capability config must be a JSON object".to_string())
}
}