sim-lib-doc-store 0.1.0

Relational document store projections for SIM office documents.
Documentation
use std::{fmt, sync::Arc};

use serde::{Deserialize, Serialize};
use sim_kernel::{
    Cx, Datum, DefaultFactory, HandleSeed, NoopEvalPolicy, NumberLiteral, Symbol, Value,
    value_from_datum,
};
use sim_lib_doc_core::{Doc, DocId, DocKind, Edit, ExternalRef};

#[derive(Debug)]
pub(crate) struct CodecError {
    message: String,
}

impl CodecError {
    pub(crate) fn new(message: impl Into<String>) -> Self {
        Self {
            message: message.into(),
        }
    }
}

impl fmt::Display for CodecError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.message)
    }
}

impl std::error::Error for CodecError {}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
struct StoredSymbol {
    namespace: Option<String>,
    name: String,
}

impl StoredSymbol {
    fn from_symbol(symbol: &Symbol) -> Self {
        Self {
            namespace: symbol.namespace.as_ref().map(ToString::to_string),
            name: symbol.name.to_string(),
        }
    }

    fn into_symbol(self) -> Symbol {
        match self.namespace {
            Some(namespace) => Symbol::qualified(namespace, self.name),
            None => Symbol::new(self.name),
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
enum StoredDatum {
    Nil,
    Bool {
        value: bool,
    },
    Number {
        domain: StoredSymbol,
        canonical: String,
    },
    Symbol {
        symbol: StoredSymbol,
    },
    String {
        value: String,
    },
    Bytes {
        value: Vec<u8>,
    },
    List {
        items: Vec<StoredDatum>,
    },
    Vector {
        items: Vec<StoredDatum>,
    },
    Map {
        entries: Vec<(StoredDatum, StoredDatum)>,
    },
    Set {
        items: Vec<StoredDatum>,
    },
    Node {
        tag: StoredSymbol,
        fields: Vec<(StoredSymbol, StoredDatum)>,
    },
}

impl StoredDatum {
    fn from_datum(datum: &Datum) -> Self {
        match datum {
            Datum::Nil => Self::Nil,
            Datum::Bool(value) => Self::Bool { value: *value },
            Datum::Number(number) => Self::Number {
                domain: StoredSymbol::from_symbol(&number.domain),
                canonical: number.canonical.clone(),
            },
            Datum::Symbol(symbol) => Self::Symbol {
                symbol: StoredSymbol::from_symbol(symbol),
            },
            Datum::String(value) => Self::String {
                value: value.clone(),
            },
            Datum::Bytes(value) => Self::Bytes {
                value: value.clone(),
            },
            Datum::List(items) => Self::List {
                items: items.iter().map(Self::from_datum).collect(),
            },
            Datum::Vector(items) => Self::Vector {
                items: items.iter().map(Self::from_datum).collect(),
            },
            Datum::Map(entries) => Self::Map {
                entries: entries
                    .iter()
                    .map(|(key, value)| (Self::from_datum(key), Self::from_datum(value)))
                    .collect(),
            },
            Datum::Set(items) => Self::Set {
                items: items.iter().map(Self::from_datum).collect(),
            },
            Datum::Node { tag, fields } => Self::Node {
                tag: StoredSymbol::from_symbol(tag),
                fields: fields
                    .iter()
                    .map(|(name, value)| (StoredSymbol::from_symbol(name), Self::from_datum(value)))
                    .collect(),
            },
        }
    }

    fn into_datum(self) -> Datum {
        match self {
            Self::Nil => Datum::Nil,
            Self::Bool { value } => Datum::Bool(value),
            Self::Number { domain, canonical } => Datum::Number(NumberLiteral {
                domain: domain.into_symbol(),
                canonical,
            }),
            Self::Symbol { symbol } => Datum::Symbol(symbol.into_symbol()),
            Self::String { value } => Datum::String(value),
            Self::Bytes { value } => Datum::Bytes(value),
            Self::List { items } => Datum::List(items.into_iter().map(Self::into_datum).collect()),
            Self::Vector { items } => {
                Datum::Vector(items.into_iter().map(Self::into_datum).collect())
            }
            Self::Map { entries } => Datum::Map(
                entries
                    .into_iter()
                    .map(|(key, value)| (key.into_datum(), value.into_datum()))
                    .collect(),
            ),
            Self::Set { items } => Datum::Set(items.into_iter().map(Self::into_datum).collect()),
            Self::Node { tag, fields } => Datum::Node {
                tag: tag.into_symbol(),
                fields: fields
                    .into_iter()
                    .map(|(name, value)| (name.into_symbol(), value.into_datum()))
                    .collect(),
            },
        }
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
struct StoredDoc {
    id: String,
    kind: String,
    body: StoredDatum,
    origin: Vec<ExternalRef>,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
struct StoredEdit {
    doc: String,
    domain: String,
    op: StoredDatum,
    inverse: StoredDatum,
}

pub(crate) fn encode_doc(doc: &Doc) -> Result<String, CodecError> {
    let stored = StoredDoc {
        id: doc.id.as_str().to_owned(),
        kind: doc.kind.as_str().to_owned(),
        body: snapshot_value(&doc.body)?,
        origin: doc.origin.clone(),
    };
    serde_json::to_string(&stored).map_err(|error| CodecError::new(error.to_string()))
}

pub(crate) fn decode_doc(encoded: &str) -> Result<Doc, CodecError> {
    let stored: StoredDoc =
        serde_json::from_str(encoded).map_err(|error| CodecError::new(error.to_string()))?;
    Ok(Doc::new(
        DocKind::new(stored.kind),
        DocId::new(stored.id),
        value_from_stored(stored.body)?,
        stored.origin,
    ))
}

pub(crate) fn encode_edit(edit: &Edit) -> Result<String, CodecError> {
    let stored = StoredEdit {
        doc: edit.doc.as_str().to_owned(),
        domain: edit.domain.clone(),
        op: snapshot_value(&edit.op)?,
        inverse: snapshot_value(&edit.inverse)?,
    };
    serde_json::to_string(&stored).map_err(|error| CodecError::new(error.to_string()))
}

pub(crate) fn decode_edit(encoded: &str) -> Result<Edit, CodecError> {
    let stored: StoredEdit =
        serde_json::from_str(encoded).map_err(|error| CodecError::new(error.to_string()))?;
    Ok(Edit::new(
        DocId::new(stored.doc),
        stored.domain,
        value_from_stored(stored.op)?,
        value_from_stored(stored.inverse)?,
    ))
}

fn snapshot_value(value: &Value) -> Result<StoredDatum, CodecError> {
    let mut cx = scratch_cx();
    let datum = value
        .object()
        .snapshot(&mut cx)
        .map_err(|error| CodecError::new(format!("value snapshot failed: {error}")))?
        .ok_or_else(|| CodecError::new("value does not expose a pure datum snapshot"))?;
    Ok(StoredDatum::from_datum(&datum))
}

fn value_from_stored(stored: StoredDatum) -> Result<Value, CodecError> {
    let mut cx = scratch_cx();
    value_from_datum(&mut cx, stored.into_datum())
        .map_err(|error| CodecError::new(format!("value restore failed: {error}")))
}

fn scratch_cx() -> Cx {
    Cx::new(
        Arc::new(NoopEvalPolicy),
        Arc::new(DefaultFactory),
        HandleSeed::new(0x444f_4353),
    )
}