Skip to main content

kcode_kweb_db/
model.rs

1use crate::{NodeId, ObjectId, TransactionId, WriterId};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use std::{fmt, sync::Arc};
5
6pub(crate) const MAX_STRING_BYTES: usize = 1024 * 1024;
7pub(crate) const MAX_TRANSACTION_BYTES: usize = 16 * 1024 * 1024;
8pub(crate) const MAX_OBJECT_BYTES: usize = 64 * 1024 * 1024;
9pub(crate) const MAX_ITEMS: usize = 1_000_000;
10
11pub type Result<T> = std::result::Result<T, Error>;
12
13#[derive(Debug)]
14pub enum Error {
15    Io(std::io::Error),
16    Busy(String),
17    Corrupt(String),
18    InvalidConfig(String),
19    InvalidInput(String),
20    InvalidTransaction(String),
21    NotFound(String),
22}
23
24impl Error {
25    pub(crate) fn corrupt(message: impl Into<String>) -> Self {
26        Self::Corrupt(message.into())
27    }
28
29    pub(crate) fn invalid_config(message: impl Into<String>) -> Self {
30        Self::InvalidConfig(message.into())
31    }
32
33    pub(crate) fn invalid_input(message: impl Into<String>) -> Self {
34        Self::InvalidInput(message.into())
35    }
36
37    pub(crate) fn invalid_transaction(message: impl Into<String>) -> Self {
38        Self::InvalidTransaction(message.into())
39    }
40
41    pub(crate) fn not_found(message: impl Into<String>) -> Self {
42        Self::NotFound(message.into())
43    }
44}
45
46impl fmt::Display for Error {
47    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
48        match self {
49            Self::Io(error) => write!(formatter, "I/O error: {error}"),
50            Self::Busy(message) => write!(formatter, "database is busy: {message}"),
51            Self::Corrupt(message) => write!(formatter, "database corruption: {message}"),
52            Self::InvalidConfig(message) => write!(formatter, "invalid configuration: {message}"),
53            Self::InvalidInput(message) => write!(formatter, "invalid input: {message}"),
54            Self::InvalidTransaction(message) => {
55                write!(formatter, "invalid transaction: {message}")
56            }
57            Self::NotFound(message) => write!(formatter, "not found: {message}"),
58        }
59    }
60}
61
62impl std::error::Error for Error {
63    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
64        match self {
65            Self::Io(error) => Some(error),
66            _ => None,
67        }
68    }
69}
70
71impl From<std::io::Error> for Error {
72    fn from(error: std::io::Error) -> Self {
73        Self::Io(error)
74    }
75}
76
77pub trait Gossip: Send + Sync + 'static {
78    fn announce(&self, package: TransactionPackage);
79}
80
81#[derive(Clone, Copy, Debug, Default)]
82pub struct NoopGossip;
83
84impl Gossip for NoopGossip {
85    fn announce(&self, _package: TransactionPackage) {}
86}
87
88#[derive(Clone)]
89pub struct Config {
90    pub signing_key: [u8; 32],
91    pub writers_by_priority: Vec<WriterId>,
92    pub gossip: Arc<dyn Gossip>,
93}
94
95impl fmt::Debug for Config {
96    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
97        formatter
98            .debug_struct("Config")
99            .field("signing_key", &"[redacted]")
100            .field("writers_by_priority", &self.writers_by_priority)
101            .field("gossip", &"dyn Gossip")
102            .finish()
103    }
104}
105
106#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
107pub struct Provenance {
108    pub author: String,
109    pub source: String,
110    pub source_created_at: DateTime<Utc>,
111    pub data: String,
112}
113
114impl Provenance {
115    pub(crate) fn validate(&self) -> Result<()> {
116        if self.author.trim().is_empty() || self.author.len() > MAX_STRING_BYTES {
117            return Err(Error::invalid_input(
118                "provenance author is empty or too large",
119            ));
120        }
121        if self.source.trim().is_empty() || self.source.len() > MAX_STRING_BYTES {
122            return Err(Error::invalid_input(
123                "provenance source is empty or too large",
124            ));
125        }
126        if self.data.len() > MAX_STRING_BYTES {
127            return Err(Error::invalid_input("provenance data exceeds 1 MiB"));
128        }
129        Ok(())
130    }
131}
132
133#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
134pub enum Owner {
135    Unowned,
136    SelfNode,
137    Node(NodeId),
138}
139
140#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
141pub struct NodeData {
142    pub short_name: String,
143    pub short_description: String,
144    pub long_description: String,
145    pub owner: Owner,
146    pub fixed_connections: [Option<NodeId>; 3],
147    pub objects: Vec<ObjectId>,
148}
149
150impl NodeData {
151    pub(crate) fn validate(&self) -> Result<()> {
152        let characters = self.short_name.chars().count();
153        if !(4..=50).contains(&characters) {
154            return Err(Error::invalid_input(
155                "short_name must contain between 4 and 50 characters",
156            ));
157        }
158        if self.short_description.chars().count() > 200 {
159            return Err(Error::invalid_input(
160                "short_description exceeds 200 characters",
161            ));
162        }
163        if self.long_description.split_whitespace().count() > 1_000 {
164            return Err(Error::invalid_input("long_description exceeds 1,000 words"));
165        }
166        if self.short_name.len() > MAX_STRING_BYTES
167            || self.short_description.len() > MAX_STRING_BYTES
168            || self.long_description.len() > MAX_STRING_BYTES
169        {
170            return Err(Error::invalid_input("node text exceeds 1 MiB"));
171        }
172        let fixed = self
173            .fixed_connections
174            .iter()
175            .flatten()
176            .copied()
177            .collect::<std::collections::BTreeSet<_>>();
178        if fixed.len() != self.fixed_connections.iter().flatten().count() {
179            return Err(Error::invalid_input("fixed connections must be unique"));
180        }
181        let objects = self
182            .objects
183            .iter()
184            .copied()
185            .collect::<std::collections::BTreeSet<_>>();
186        if objects.len() != self.objects.len() {
187            return Err(Error::invalid_input("object references must be unique"));
188        }
189        Ok(())
190    }
191}
192
193#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
194pub struct Node {
195    pub id: NodeId,
196    pub data: NodeData,
197    pub connections: Vec<NodeId>,
198    pub last_author: String,
199    pub committed_at: DateTime<Utc>,
200}
201
202#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
203pub struct MergePair {
204    pub first: TransactionId,
205    pub second: TransactionId,
206}
207
208impl MergePair {
209    pub fn new(first: TransactionId, second: TransactionId) -> Result<Self> {
210        if first == second {
211            return Err(Error::invalid_input(
212                "a merge pair must name two different transactions",
213            ));
214        }
215        let (first, second) = if first < second {
216            (first, second)
217        } else {
218            (second, first)
219        };
220        Ok(Self { first, second })
221    }
222}
223
224#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
225pub struct HistoryEntry {
226    pub transaction_id: TransactionId,
227    pub writer: WriterId,
228    pub committed_at: DateTime<Utc>,
229    pub provenance: Provenance,
230    pub active: bool,
231    pub created: bool,
232    pub updated: bool,
233    pub connections: Vec<(NodeId, NodeId)>,
234    pub merge_pairs: Vec<MergePair>,
235}
236
237#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
238pub struct NodeHistory {
239    pub node_id: NodeId,
240    pub frontier: Vec<TransactionId>,
241    pub visible: Option<TransactionId>,
242    pub entries: Vec<HistoryEntry>,
243}
244
245#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
246pub struct ObjectPayload {
247    pub id: ObjectId,
248    pub bytes: Vec<u8>,
249}
250
251#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
252pub struct TransactionPackage {
253    pub transaction: Vec<u8>,
254    pub objects: Vec<ObjectPayload>,
255}