use super::super::{database::models::identities, database::types::IdentityKind};
use crate::backend::error::{Result, invalid};
use crate::backend::{models as m, proto::lgtm::v1 as p};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Actor {
pub id: uuid::Uuid,
pub name: String,
#[serde(flatten)]
pub kind: ActorKind,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ActorKind {
Human { email: String },
Agent { model: String, provider: String },
}
impl Actor {
pub fn validate(&self) -> Result<()> {
fn single(value: &str) -> bool {
!value.trim().is_empty() && !value.contains(['\n', '\r'])
}
if !single(&self.name) {
return Err(invalid("Identity requires a nonempty single-line name"));
}
match &self.kind {
ActorKind::Human { email } if !single(email) || !email.contains('@') => {
Err(invalid("Human identity requires an email"))
}
ActorKind::Agent { model, provider } if !single(model) || !single(provider) => {
Err(invalid("Agent identity requires a model and provider"))
}
_ => Ok(()),
}
}
pub fn email(&self) -> Option<&str> {
match &self.kind {
ActorKind::Human { email } => Some(email),
_ => None,
}
}
pub fn model(&self) -> Option<&str> {
match &self.kind {
ActorKind::Agent { model, .. } => Some(model),
_ => None,
}
}
pub fn label(&self) -> String {
match &self.kind {
ActorKind::Human { email } => format!("{} <{}>", self.name, email),
ActorKind::Agent { model, provider } => format!("{} ({provider}/{model})", self.name),
}
}
pub(crate) fn into_proto(self) -> p::Actor {
let v = self;
let mut out = p::Actor {
id: v.id.to_string(),
name: v.name,
..Default::default()
};
match v.kind {
m::ActorKind::Human { email } => {
out.kind = "human".into();
out.email = email;
}
m::ActorKind::Agent { model, provider } => {
out.kind = "agent".into();
out.model = model;
out.provider = provider;
}
}
out
}
}
impl From<&Actor> for identities::Model {
fn from(actor: &Actor) -> Self {
let (kind, email, model, provider) = match &actor.kind {
ActorKind::Human { email } => (IdentityKind::Human, Some(email.clone()), None, None),
ActorKind::Agent { model, provider } => (
IdentityKind::Agent,
None,
Some(model.clone()),
Some(provider.clone()),
),
};
Self {
id: actor.id,
name: actor.name.clone(),
kind,
email,
model,
provider,
forge_account: String::new(),
}
}
}
impl TryFrom<identities::Model> for Actor {
type Error = super::super::error::Error;
fn try_from(row: identities::Model) -> Result<Self> {
let kind = match row.kind {
IdentityKind::Human => ActorKind::Human {
email: row
.email
.ok_or_else(|| invalid("Human identity missing email"))?,
},
IdentityKind::Agent => ActorKind::Agent {
model: row
.model
.ok_or_else(|| invalid("Agent identity missing model"))?,
provider: row
.provider
.ok_or_else(|| invalid("Agent identity missing provider"))?,
},
};
let actor = Self {
id: row.id,
name: row.name,
kind,
};
actor.validate()?;
Ok(actor)
}
}