Skip to main content

systemprompt_models/brain/
subject.rs

1//! The business subject a fact is about, written `<kind>:<id>`, such as
2//! `client:acme` or `service:billing`.
3//!
4//! Both halves are bounded ([`MAX_SUBJECT_KIND_LEN`],
5//! [`MAX_SUBJECT_ID_LEN`]) to what the subject table admits, so a subject
6//! that could never be indexed is refused where it enters rather than when
7//! a consumer round tries to write its row.
8//!
9//! Copyright (c) systemprompt.io — Business Source License 1.1.
10//! See <https://systemprompt.io> for licensing details.
11
12use std::fmt;
13use std::str::FromStr;
14
15use schemars::JsonSchema;
16use serde::{Deserialize, Serialize};
17
18// Why: Postgres — brain_subjects carries CHECK (length(subject_kind)
19// BETWEEN 1 AND 64) and CHECK (length(subject_id) BETWEEN 1 AND 255),
20// counted in characters; brain_facts carries no such check.
21pub const MAX_SUBJECT_KIND_LEN: usize = 64;
22pub const MAX_SUBJECT_ID_LEN: usize = 255;
23
24/// A business subject: the subject kind a fact kind declares and the
25/// subject's key within it.
26#[derive(
27    Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
28)]
29#[serde(deny_unknown_fields)]
30pub struct SubjectKey {
31    pub kind: String,
32    pub id: String,
33}
34
35/// Why a subject key is not one the store admits.
36#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
37pub enum SubjectKeyError {
38    #[error("invalid subject key {0:?}: expected `<kind>:<id>`")]
39    Shape(String),
40    #[error(
41        "subject kind is {len} characters; between 1 and {} are allowed",
42        MAX_SUBJECT_KIND_LEN
43    )]
44    KindLength { len: usize },
45    #[error(
46        "subject id is {len} characters; between 1 and {} are allowed",
47        MAX_SUBJECT_ID_LEN
48    )]
49    IdLength { len: usize },
50}
51
52impl SubjectKey {
53    pub fn new(kind: impl Into<String>, id: impl Into<String>) -> Self {
54        Self {
55            kind: kind.into(),
56            id: id.into(),
57        }
58    }
59
60    pub fn try_new(
61        kind: impl Into<String>,
62        id: impl Into<String>,
63    ) -> Result<Self, SubjectKeyError> {
64        let key = Self::new(kind, id);
65        key.validate()?;
66        Ok(key)
67    }
68
69    pub fn validate(&self) -> Result<(), SubjectKeyError> {
70        let kind = self.kind.chars().count();
71        if !(1..=MAX_SUBJECT_KIND_LEN).contains(&kind) {
72            return Err(SubjectKeyError::KindLength { len: kind });
73        }
74        let id = self.id.chars().count();
75        if !(1..=MAX_SUBJECT_ID_LEN).contains(&id) {
76            return Err(SubjectKeyError::IdLength { len: id });
77        }
78        Ok(())
79    }
80}
81
82impl fmt::Display for SubjectKey {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        write!(f, "{}:{}", self.kind, self.id)
85    }
86}
87
88impl FromStr for SubjectKey {
89    type Err = SubjectKeyError;
90
91    fn from_str(s: &str) -> Result<Self, Self::Err> {
92        match s.split_once(':') {
93            Some((kind, id)) if !kind.is_empty() && !id.is_empty() => Self::try_new(kind, id),
94            _ => Err(SubjectKeyError::Shape(s.to_owned())),
95        }
96    }
97}