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,
revision: Option<RustTxId>,
document: RustCodeDocument,
}
impl OpenedDocument {
pub fn selector(&self) -> &SourceSelector {
&self.selector
}
pub const fn revision(&self) -> Option<RustTxId> {
self.revision
}
pub fn document(&self) -> &RustCodeDocument {
&self.document
}
fn from_source(source: OpenedSource) -> Result<Self, RustCodeKtoolServiceError> {
let selector = source.selector().clone();
let revision = source.revision();
let document = RustCodeDocument::from_source_package(source.into_source())
.map_err(RustCodeKtoolServiceError::Unsupported)?;
Ok(Self {
selector,
revision,
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> {
let source = self
.projection
.open_source(family, selector)
.map_err(RustCodeKtoolServiceError::Projection)?
.ok_or(RustCodeKtoolServiceError::LibraryAbsent)?;
if matches!(source.selector(), SourceSelector::Unpublished(_)) {
self.authorize(authenticated, family)?;
}
OpenedDocument::from_source(source)
}
pub fn latest_published(
&self,
family: &LibraryFamily,
) -> Result<OpenedDocument, RustCodeKtoolServiceError> {
if let Some(source) = self
.projection
.latest_published(family)
.map_err(RustCodeKtoolServiceError::Projection)?
{
return OpenedDocument::from_source(source);
}
if self
.projection
.family_exists(family)
.map_err(RustCodeKtoolServiceError::Projection)?
{
Err(RustCodeKtoolServiceError::NoPublishedVersion)
} else {
Err(RustCodeKtoolServiceError::LibraryAbsent)
}
}
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) => {
if opened.revision().is_some() {
return Err(RustCodeKtoolServiceError::WorktreeChanged);
}
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 expected_revision = opened
.revision()
.ok_or(RustCodeKtoolServiceError::WorktreeChanged)?;
let current = self
.projection
.load_unpublished(family, *id)
.map_err(RustCodeKtoolServiceError::Projection)?
.ok_or(RustCodeKtoolServiceError::WorktreeChanged)?;
if current.revision() != expected_revision
|| current.source() != &opened.document.clone().into_source_package()
{
return Err(RustCodeKtoolServiceError::WorktreeChanged);
}
self.projection
.overwrite(*id, expected_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))
}
}