kcode-k1-rust-code-ktool-service 0.1.2

Coordinate authenticated persistent K1 Rust code operations
Documentation
use kcode_k1_access_types::{AccessContext, Authority, GroupId, TxId as AccessTxId, UserId};
use kcode_k1_groups::K1Groups;
use kcode_k1_objects::K1Objects;
use kcode_k1_rust_code_document::{RustCodeDocument, RustCodeDocumentError};
use kcode_k1_rust_coding::{
    K1RustCoding, RustCodingConfig, RustCodingError, RustPublishAuthorization, VerifiedCheck,
};
use kcode_k1_rust_package::{LibraryFamily, SourcePackage};
use kcode_k1_rust_projection::{K1RustProjection, PublishOutcome};
use kcode_k1_rust_transaction::{RustSourceTransaction, TransactionError, encode};
use kcode_k1_rust_worktree::{OpenedSource, SourceSelector, publication_source};
use kcode_k1_transaction_id::TxId as RustTxId;
use semver::Version;
use std::collections::HashMap;
use std::error::Error;
use std::fmt::{Debug, Display, Formatter};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};

const OBJECT_FILENAME: &str = "rust-publication.k1rust";
const OBJECT_MEDIA_TYPE: &str = "application/vnd.kennedy.k1-rust-source-transaction.v1";

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OpenedDocument {
    selector: SourceSelector,
    document: RustCodeDocument,
}

impl OpenedDocument {
    pub fn selector(&self) -> &SourceSelector {
        &self.selector
    }

    pub fn document(&self) -> &RustCodeDocument {
        &self.document
    }

    fn from_source(source: OpenedSource) -> Result<Self, RustCodeKtoolServiceError> {
        let selector = source.selector().clone();
        let document = RustCodeDocument::from_source_package(source.into_source())
            .map_err(RustCodeKtoolServiceError::Unsupported)?;
        Ok(Self { selector, document })
    }
}

#[derive(Debug)]
pub enum RustCodeKtoolServiceError {
    State(String),
    Authorization(String),
    AccessDenied,
    LibraryExists,
    LibraryAbsent,
    NoPublishedVersion,
    WorktreeChanged,
    VersionUsed,
    Unsupported(RustCodeDocumentError),
    CodingInitialization(String),
    Coding(RustCodingError),
    SourceEncoding(TransactionError),
    Object(String),
    CheckMismatch,
    Projection(String),
}

impl Display for RustCodeKtoolServiceError {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        Debug::fmt(self, formatter)
    }
}

impl Error for RustCodeKtoolServiceError {}

pub struct ServiceCheck {
    user: UserId,
    package: SourcePackage,
    verified: VerifiedCheck,
}

#[derive(Default)]
struct UserCoding {
    value: OnceLock<Result<Arc<Mutex<K1RustCoding>>, String>>,
}

pub struct RustCodeKtoolService {
    cache_root: PathBuf,
    config: RustCodingConfig,
    projection: Arc<K1RustProjection>,
    objects: Arc<K1Objects>,
    groups: Arc<K1Groups>,
    users: Mutex<HashMap<UserId, Arc<UserCoding>>>,
}

impl RustCodeKtoolService {
    pub fn new(
        cache_root: impl AsRef<Path>,
        config: RustCodingConfig,
        projection: Arc<K1RustProjection>,
        objects: Arc<K1Objects>,
        groups: Arc<K1Groups>,
    ) -> Self {
        Self {
            cache_root: cache_root.as_ref().to_path_buf(),
            config,
            projection,
            objects,
            groups,
            users: Mutex::new(HashMap::new()),
        }
    }

    pub fn create(
        &self,
        authenticated: &AccessContext,
        family: LibraryFamily,
    ) -> Result<OpenedDocument, RustCodeKtoolServiceError> {
        self.authorize(authenticated, &family)?;
        if self
            .projection
            .family_exists(&family)
            .map_err(RustCodeKtoolServiceError::Projection)?
        {
            return Err(RustCodeKtoolServiceError::LibraryExists);
        }
        let identity = kcode_k1_rust_package::LibraryId::new(family, Version::new(0, 0, 0))
            .map_err(|error| RustCodeKtoolServiceError::State(error.to_string()))?;
        let document = RustCodeDocument::approved_default(identity)
            .map_err(RustCodeKtoolServiceError::Unsupported)?;
        let worktree = self
            .projection
            .create(&document.clone().into_source_package())
            .map_err(map_write_error)?;
        OpenedDocument::from_source(OpenedSource::unpublished(&worktree))
    }

    pub fn open(
        &self,
        authenticated: &AccessContext,
        family: &LibraryFamily,
        selector: Option<&SourceSelector>,
    ) -> Result<OpenedDocument, RustCodeKtoolServiceError> {
        if !matches!(selector, Some(SourceSelector::Published(_))) {
            self.authorize(authenticated, family)?;
        }
        let source = self
            .projection
            .open_source(family, selector)
            .map_err(RustCodeKtoolServiceError::Projection)?
            .ok_or(RustCodeKtoolServiceError::LibraryAbsent)?;
        OpenedDocument::from_source(source)
    }

    pub fn latest_published(
        &self,
        family: &LibraryFamily,
    ) -> Result<OpenedDocument, RustCodeKtoolServiceError> {
        let source = self
            .projection
            .latest_published(family)
            .map_err(RustCodeKtoolServiceError::Projection)?
            .ok_or_else(|| {
                if self.projection.family_exists(family).unwrap_or(false) {
                    RustCodeKtoolServiceError::NoPublishedVersion
                } else {
                    RustCodeKtoolServiceError::LibraryAbsent
                }
            })?;
        OpenedDocument::from_source(source)
    }

    pub fn overwrite(
        &self,
        authenticated: &AccessContext,
        opened: &OpenedDocument,
        next: RustCodeDocument,
    ) -> Result<OpenedDocument, RustCodeKtoolServiceError> {
        let family = opened.document.identity().family();
        self.authorize(authenticated, family)?;
        if next.identity() != opened.document.identity() {
            return Err(RustCodeKtoolServiceError::WorktreeChanged);
        }
        let source = next.into_source_package();
        let worktree = match opened.selector() {
            SourceSelector::Published(version) => {
                let published = self
                    .projection
                    .open_source(family, Some(&SourceSelector::Published(version.clone())))
                    .map_err(RustCodeKtoolServiceError::Projection)?
                    .ok_or(RustCodeKtoolServiceError::WorktreeChanged)?;
                if published.source() != &opened.document.clone().into_source_package() {
                    return Err(RustCodeKtoolServiceError::WorktreeChanged);
                }
                self.projection.fork(&source).map_err(map_write_error)?
            }
            SourceSelector::Unpublished(id) => {
                let current = self
                    .projection
                    .load_unpublished(family, *id)
                    .map_err(RustCodeKtoolServiceError::Projection)?
                    .ok_or(RustCodeKtoolServiceError::WorktreeChanged)?;
                if current.source() != &opened.document.clone().into_source_package() {
                    return Err(RustCodeKtoolServiceError::WorktreeChanged);
                }
                self.projection
                    .overwrite(*id, current.revision(), &source)
                    .map_err(map_write_error)?
            }
        };
        OpenedDocument::from_source(OpenedSource::unpublished(&worktree))
    }

    pub fn versioned(
        &self,
        document: &RustCodeDocument,
        version: Version,
    ) -> Result<RustCodeDocument, RustCodeKtoolServiceError> {
        let package = publication_source(&document.clone().into_source_package(), version)
            .map_err(|error| RustCodeKtoolServiceError::State(error.to_string()))?;
        RustCodeDocument::from_source_package(package)
            .map_err(RustCodeKtoolServiceError::Unsupported)
    }

    pub fn check_fresh(
        &self,
        authenticated: &AccessContext,
        document: &RustCodeDocument,
    ) -> Result<ServiceCheck, RustCodeKtoolServiceError> {
        let package = document.clone().into_source_package();
        let coding = self.coding(authenticated)?;
        let mut coding = coding
            .lock()
            .map_err(|_| RustCodeKtoolServiceError::State("user coding mutex poisoned".into()))?;
        let verified = coding
            .check_fresh(&package)
            .map_err(RustCodeKtoolServiceError::Coding)?;
        Ok(ServiceCheck {
            user: authenticated.user(),
            package,
            verified,
        })
    }

    pub fn current_check(
        &self,
        authenticated: &AccessContext,
        document: &RustCodeDocument,
    ) -> Result<Option<ServiceCheck>, RustCodeKtoolServiceError> {
        let package = document.clone().into_source_package();
        let coding = self.coding(authenticated)?;
        let mut coding = coding
            .lock()
            .map_err(|_| RustCodeKtoolServiceError::State("user coding mutex poisoned".into()))?;
        let verified = coding
            .current_check(&package)
            .map_err(RustCodeKtoolServiceError::Coding)?;
        Ok(verified.map(|verified| ServiceCheck {
            user: authenticated.user(),
            package,
            verified,
        }))
    }

    pub fn publish_checked(
        &self,
        authenticated: &AccessContext,
        document: &RustCodeDocument,
        checked: ServiceCheck,
    ) -> Result<PublishOutcome, RustCodeKtoolServiceError> {
        let package = document.clone().into_source_package();
        if checked.user != authenticated.user() || checked.package != package {
            return Err(RustCodeKtoolServiceError::CheckMismatch);
        }
        if self
            .projection
            .version_exists(package.id())
            .map_err(RustCodeKtoolServiceError::Projection)?
        {
            return Err(RustCodeKtoolServiceError::VersionUsed);
        }
        let coding = self.coding(authenticated)?;
        let mut coding = coding
            .lock()
            .map_err(|_| RustCodeKtoolServiceError::State("user coding mutex poisoned".into()))?;
        let bytes = encode(&RustSourceTransaction::Publish(package.clone()))
            .map_err(RustCodeKtoolServiceError::SourceEncoding)?;
        self.objects
            .save(OBJECT_FILENAME, OBJECT_MEDIA_TYPE, "", &bytes)
            .map_err(RustCodeKtoolServiceError::Object)?;
        let gate = AuthorityGate {
            authenticated,
            groups: &self.groups,
        };
        coding
            .publish_checked(&package, &checked.verified, &gate)
            .map_err(RustCodeKtoolServiceError::Coding)
    }

    fn authorize(
        &self,
        authenticated: &AccessContext,
        family: &LibraryFamily,
    ) -> Result<(), RustCodeKtoolServiceError> {
        match (AuthorityGate {
            authenticated,
            groups: &self.groups,
        })
        .authorize_publish(family)
        {
            Ok(true) => Ok(()),
            Ok(false) => Err(RustCodeKtoolServiceError::AccessDenied),
            Err(error) => Err(RustCodeKtoolServiceError::Authorization(error)),
        }
    }

    fn coding(
        &self,
        authenticated: &AccessContext,
    ) -> Result<Arc<Mutex<K1RustCoding>>, RustCodeKtoolServiceError> {
        let slot = {
            let mut users = self.users.lock().map_err(|_| {
                RustCodeKtoolServiceError::State("user coding map mutex poisoned".into())
            })?;
            users
                .entry(authenticated.user())
                .or_insert_with(|| Arc::new(UserCoding::default()))
                .clone()
        };
        match slot.value.get_or_init(|| {
            let user = RustTxId::from_bytes(*authenticated.user().as_tx_id().as_bytes());
            K1RustCoding::open(
                &self.cache_root,
                user,
                self.config.clone(),
                self.projection.clone(),
            )
            .map(|coding| Arc::new(Mutex::new(coding)))
            .map_err(|error| error.to_string())
        }) {
            Ok(coding) => Ok(coding.clone()),
            Err(error) => Err(RustCodeKtoolServiceError::CodingInitialization(
                error.clone(),
            )),
        }
    }
}

fn map_write_error(error: String) -> RustCodeKtoolServiceError {
    if error == "unpublished version changed" || error == "source transaction was rejected" {
        RustCodeKtoolServiceError::WorktreeChanged
    } else if error == "library family already exists" {
        RustCodeKtoolServiceError::LibraryExists
    } else {
        RustCodeKtoolServiceError::Projection(error)
    }
}

struct AuthorityGate<'a> {
    authenticated: &'a AccessContext,
    groups: &'a K1Groups,
}

impl RustPublishAuthorization for AuthorityGate<'_> {
    fn authorize_publish(&self, family: &LibraryFamily) -> Result<bool, String> {
        let authority = family.authority();
        let authority = authority.transaction_id();
        if authority.as_bytes() == self.authenticated.user().as_tx_id().as_bytes() {
            return Ok(true);
        }
        let group = GroupId::new(AccessTxId::from_bytes(*authority.as_bytes()));
        if group.sentinel().is_some()
            || self
                .authenticated
                .filter()
                .contains(Authority::Group(group))
        {
            return Ok(false);
        }
        let memberships = self
            .groups
            .memberships(self.authenticated.user(), self.authenticated.model())?;
        Ok(memberships.shared_groups().contains(&group))
    }
}