Skip to main content

pamin_kit/
seams.rs

1//! Where the platform plugs in: who is calling, what they may use, and what
2//! they used. Each seam has an open-source default; the SaaS implements them in
3//! `pamin-platform`.
4
5use std::future::Future;
6use std::sync::Arc;
7use std::time::Duration;
8
9/// Who a call is for and what it works on. It comes only from the caller's
10/// identity, never from tool arguments.
11#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
12pub struct Space {
13    pub tenant: String,
14    pub user: String,
15    pub project: String,
16}
17
18/// What an operation needs the caller to be entitled to.
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
20pub enum Feature {
21    /// Part of every edition.
22    Core,
23    /// A named paid feature.
24    Pro(&'static str),
25}
26
27/// Resolves a request to the space it works on.
28pub trait Identity: Send + Sync + 'static {
29    /// The caller's space, or `None` to refuse the call. `bearer` is the
30    /// request's bearer token, absent over stdio.
31    fn identify(&self, bearer: Option<&str>) -> impl Future<Output = Option<Arc<Space>>> + Send;
32}
33
34/// Decides whether a space may use a feature.
35pub trait Entitlements: Send + Sync + 'static {
36    fn allows(&self, space: &Space, feature: Feature) -> bool;
37}
38
39/// Records what each call used.
40pub trait Metering: Send + Sync + 'static {
41    fn record(&self, space: &Space, usage: Usage);
42}
43
44/// One call, as metering sees it.
45#[derive(Clone, Copy, Debug)]
46pub struct Usage {
47    pub operation: &'static str,
48    pub feature: Feature,
49    pub elapsed: Duration,
50    /// Whether the operation succeeded.
51    pub ok: bool,
52}
53
54/// The open-source identity: one local user working on one project, whatever
55/// the request carries.
56#[derive(Clone, Debug)]
57pub struct Local {
58    space: Arc<Space>,
59}
60
61impl Local {
62    /// The project named by the working directory, canonicalized so that
63    /// two paths to one directory are one project.
64    pub fn from_working_directory() -> std::io::Result<Self> {
65        let directory = std::env::current_dir()?.canonicalize()?;
66        Ok(Self::new(directory.to_string_lossy()))
67    }
68
69    pub fn new(project: impl Into<String>) -> Self {
70        Self {
71            space: Arc::new(Space {
72                tenant: "local".into(),
73                user: "local".into(),
74                project: project.into(),
75            }),
76        }
77    }
78}
79
80impl Identity for Local {
81    async fn identify(&self, _bearer: Option<&str>) -> Option<Arc<Space>> {
82        Some(self.space.clone())
83    }
84}
85
86/// The open-source entitlements: every core feature, no paid one.
87#[derive(Clone, Copy, Debug, Default)]
88pub struct Open;
89
90impl Entitlements for Open {
91    fn allows(&self, _space: &Space, feature: Feature) -> bool {
92        feature == Feature::Core
93    }
94}
95
96/// The open-source metering: records nothing.
97#[derive(Clone, Copy, Debug, Default)]
98pub struct Unmetered;
99
100impl Metering for Unmetered {
101    fn record(&self, _space: &Space, _usage: Usage) {}
102}
103
104/// A resource that changed, in the space whose call changed it.
105#[derive(Clone, Debug, PartialEq, Eq)]
106pub struct Change {
107    pub space: Arc<Space>,
108    pub uri: String,
109}
110
111/// Carries resource changes from the calls that make them to the clients
112/// listening for them (`subscriptions/listen`). Used when a call changes a
113/// resource and when a listener starts, never on the call path otherwise.
114pub trait ChangeFeed: Send + Sync + 'static {
115    /// Publishes a change, without waiting on listeners.
116    fn publish(&self, change: Change);
117
118    /// Every change from now on, in every space; the kit keeps each
119    /// listener to its own space.
120    fn subscribe(&self) -> tokio::sync::broadcast::Receiver<Change>;
121}
122
123/// The open-source feed: changes reach listeners in this process. Enough for
124/// one local server, and for a module routed `sharded_by_project`, whose
125/// project's writers and listeners share an instance; a feed across
126/// instances (PostgreSQL `LISTEN`, Redis) implements [`ChangeFeed`].
127#[derive(Debug)]
128pub struct InProcessChanges(tokio::sync::broadcast::Sender<Change>);
129
130/// How many changes a slow listener may fall behind before it skips some.
131const BACKLOG: usize = 1024;
132
133impl Default for InProcessChanges {
134    fn default() -> Self {
135        Self(tokio::sync::broadcast::channel(BACKLOG).0)
136    }
137}
138
139impl ChangeFeed for InProcessChanges {
140    fn publish(&self, change: Change) {
141        // Nobody listening is not an error.
142        let _ = self.0.send(change);
143    }
144
145    fn subscribe(&self) -> tokio::sync::broadcast::Receiver<Change> {
146        self.0.subscribe()
147    }
148}