kcode-k1-web-code-session 0.2.0

Own pure session-bound versionless Web-code handles
Documentation
#![forbid(unsafe_code)]

use kcode_k1_transaction_id::TxId;
use kcode_k1_web_code_document::{CodeDocument, DocumentError};
use semver::Version;
use std::{
    error::Error,
    fmt::{Display, Formatter},
    sync::Arc,
};

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DocumentPart {
    Documentation,
    Code,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum OpenedSource {
    Published(Version),
    Unpublished { id: TxId, revision: TxId },
}

impl OpenedSource {
    pub fn text(&self) -> String {
        match self {
            Self::Published(value) => value.to_string(),
            Self::Unpublished { id, .. } => id.to_string(),
        }
    }

    pub const fn unpublished_id(&self) -> Option<TxId> {
        match self {
            Self::Published(_) => None,
            Self::Unpublished { id, .. } => Some(*id),
        }
    }

    pub const fn revision(&self) -> Option<TxId> {
        match self {
            Self::Published(_) => None,
            Self::Unpublished { revision, .. } => Some(*revision),
        }
    }

    pub fn published_version(&self) -> Option<&Version> {
        match self {
            Self::Published(value) => Some(value),
            Self::Unpublished { .. } => None,
        }
    }
}

#[derive(Clone)]
pub struct SessionScope {
    identity: Arc<()>,
}

#[derive(Clone)]
pub struct OpenedLibrary {
    scope: Arc<()>,
    authority: [u8; 12],
    document: CodeDocument,
    source: OpenedSource,
}

impl OpenedLibrary {
    pub const fn document(&self) -> &CodeDocument {
        &self.document
    }

    pub const fn source(&self) -> &OpenedSource {
        &self.source
    }

    pub fn version_text(&self) -> String {
        self.source.text()
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SessionError {
    ForeignHandle,
    StaleSource,
    InvalidDocument(String),
}

impl Display for SessionError {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::ForeignHandle => formatter.write_str("foreign handle"),
            Self::StaleSource => formatter.write_str("stale source"),
            Self::InvalidDocument(value) => formatter.write_str(value),
        }
    }
}

impl Error for SessionError {}

impl From<DocumentError> for SessionError {
    fn from(value: DocumentError) -> Self {
        Self::InvalidDocument(value.to_string())
    }
}

impl Default for SessionScope {
    fn default() -> Self {
        Self::new()
    }
}

impl SessionScope {
    pub fn new() -> Self {
        Self {
            identity: Arc::new(()),
        }
    }

    pub fn open(
        &self,
        authority: [u8; 12],
        document: CodeDocument,
        source: OpenedSource,
    ) -> Result<OpenedLibrary, SessionError> {
        if document.family().authority().transaction_id().as_bytes() != &authority {
            return Err(SessionError::ForeignHandle);
        }
        Ok(OpenedLibrary {
            scope: self.identity.clone(),
            authority,
            document,
            source,
        })
    }

    pub fn refresh(
        &self,
        authority: [u8; 12],
        document: CodeDocument,
        source: OpenedSource,
    ) -> Result<OpenedLibrary, SessionError> {
        self.open(authority, document, source)
    }

    pub fn validate(
        &self,
        authority: [u8; 12],
        opened: &OpenedLibrary,
        document: &CodeDocument,
        source: &OpenedSource,
    ) -> Result<(), SessionError> {
        if !Arc::ptr_eq(&self.identity, &opened.scope) || opened.authority != authority {
            return Err(SessionError::ForeignHandle);
        }
        if &opened.document != document || &opened.source != source {
            return Err(SessionError::StaleSource);
        }
        Ok(())
    }

    pub fn replacement(
        &self,
        authority: [u8; 12],
        opened: &OpenedLibrary,
        document: &CodeDocument,
        source: &OpenedSource,
        part: DocumentPart,
        contents: String,
    ) -> Result<CodeDocument, SessionError> {
        self.validate(authority, opened, document, source)?;
        let (documentation, code) = match part {
            DocumentPart::Documentation => (contents.into_bytes(), document.code().to_vec()),
            DocumentPart::Code => (document.documentation().to_vec(), contents.into_bytes()),
        };
        CodeDocument::new(
            document.family().clone(),
            documentation,
            document.language(),
            code,
        )
        .map_err(Into::into)
    }
}