use crate::{NodeId, ObjectId, TransactionId, WriterId};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::{fmt, sync::Arc};
pub(crate) const MAX_STRING_BYTES: usize = 1024 * 1024;
pub(crate) const MAX_TRANSACTION_BYTES: usize = 16 * 1024 * 1024;
pub(crate) const MAX_OBJECT_BYTES: usize = 64 * 1024 * 1024;
pub(crate) const MAX_ITEMS: usize = 1_000_000;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
pub enum Error {
Io(std::io::Error),
Busy(String),
Corrupt(String),
InvalidConfig(String),
InvalidInput(String),
InvalidTransaction(String),
NotFound(String),
}
impl Error {
pub(crate) fn corrupt(message: impl Into<String>) -> Self {
Self::Corrupt(message.into())
}
pub(crate) fn invalid_config(message: impl Into<String>) -> Self {
Self::InvalidConfig(message.into())
}
pub(crate) fn invalid_input(message: impl Into<String>) -> Self {
Self::InvalidInput(message.into())
}
pub(crate) fn invalid_transaction(message: impl Into<String>) -> Self {
Self::InvalidTransaction(message.into())
}
pub(crate) fn not_found(message: impl Into<String>) -> Self {
Self::NotFound(message.into())
}
}
impl fmt::Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(error) => write!(formatter, "I/O error: {error}"),
Self::Busy(message) => write!(formatter, "database is busy: {message}"),
Self::Corrupt(message) => write!(formatter, "database corruption: {message}"),
Self::InvalidConfig(message) => write!(formatter, "invalid configuration: {message}"),
Self::InvalidInput(message) => write!(formatter, "invalid input: {message}"),
Self::InvalidTransaction(message) => {
write!(formatter, "invalid transaction: {message}")
}
Self::NotFound(message) => write!(formatter, "not found: {message}"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(error) => Some(error),
_ => None,
}
}
}
impl From<std::io::Error> for Error {
fn from(error: std::io::Error) -> Self {
Self::Io(error)
}
}
pub trait Gossip: Send + Sync + 'static {
fn announce(&self, package: TransactionPackage);
}
#[derive(Clone, Copy, Debug, Default)]
pub struct NoopGossip;
impl Gossip for NoopGossip {
fn announce(&self, _package: TransactionPackage) {}
}
#[derive(Clone)]
pub struct Config {
pub signing_key: [u8; 32],
pub writers_by_priority: Vec<WriterId>,
pub gossip: Arc<dyn Gossip>,
}
impl fmt::Debug for Config {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Config")
.field("signing_key", &"[redacted]")
.field("writers_by_priority", &self.writers_by_priority)
.field("gossip", &"dyn Gossip")
.finish()
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Provenance {
pub author: String,
pub source: String,
pub source_created_at: DateTime<Utc>,
pub data: String,
}
impl Provenance {
pub(crate) fn validate(&self) -> Result<()> {
if self.author.trim().is_empty() || self.author.len() > MAX_STRING_BYTES {
return Err(Error::invalid_input(
"provenance author is empty or too large",
));
}
if self.source.trim().is_empty() || self.source.len() > MAX_STRING_BYTES {
return Err(Error::invalid_input(
"provenance source is empty or too large",
));
}
if self.data.len() > MAX_STRING_BYTES {
return Err(Error::invalid_input("provenance data exceeds 1 MiB"));
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum Owner {
Unowned,
SelfNode,
Node(NodeId),
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct NodeData {
pub short_name: String,
pub short_description: String,
pub long_description: String,
pub owner: Owner,
pub fixed_connections: [Option<NodeId>; 3],
pub objects: Vec<ObjectId>,
}
impl NodeData {
pub(crate) fn validate(&self) -> Result<()> {
let characters = self.short_name.chars().count();
if !(4..=50).contains(&characters) {
return Err(Error::invalid_input(
"short_name must contain between 4 and 50 characters",
));
}
if self.short_description.chars().count() > 200 {
return Err(Error::invalid_input(
"short_description exceeds 200 characters",
));
}
if self.long_description.split_whitespace().count() > 1_000 {
return Err(Error::invalid_input("long_description exceeds 1,000 words"));
}
if self.short_name.len() > MAX_STRING_BYTES
|| self.short_description.len() > MAX_STRING_BYTES
|| self.long_description.len() > MAX_STRING_BYTES
{
return Err(Error::invalid_input("node text exceeds 1 MiB"));
}
let fixed = self
.fixed_connections
.iter()
.flatten()
.copied()
.collect::<std::collections::BTreeSet<_>>();
if fixed.len() != self.fixed_connections.iter().flatten().count() {
return Err(Error::invalid_input("fixed connections must be unique"));
}
let objects = self
.objects
.iter()
.copied()
.collect::<std::collections::BTreeSet<_>>();
if objects.len() != self.objects.len() {
return Err(Error::invalid_input("object references must be unique"));
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Node {
pub id: NodeId,
pub data: NodeData,
pub connections: Vec<NodeId>,
pub last_author: String,
pub committed_at: DateTime<Utc>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct MergePair {
pub first: TransactionId,
pub second: TransactionId,
}
impl MergePair {
pub fn new(first: TransactionId, second: TransactionId) -> Result<Self> {
if first == second {
return Err(Error::invalid_input(
"a merge pair must name two different transactions",
));
}
let (first, second) = if first < second {
(first, second)
} else {
(second, first)
};
Ok(Self { first, second })
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct HistoryEntry {
pub transaction_id: TransactionId,
pub writer: WriterId,
pub committed_at: DateTime<Utc>,
pub provenance: Provenance,
pub active: bool,
pub created: bool,
pub updated: bool,
pub connections: Vec<(NodeId, NodeId)>,
pub merge_pairs: Vec<MergePair>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct NodeHistory {
pub node_id: NodeId,
pub frontier: Vec<TransactionId>,
pub visible: Option<TransactionId>,
pub entries: Vec<HistoryEntry>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ObjectPayload {
pub id: ObjectId,
pub bytes: Vec<u8>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct TransactionPackage {
pub transaction: Vec<u8>,
pub objects: Vec<ObjectPayload>,
}