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, LibraryId, SourcePackage};
use kcode_k1_rust_projection::{K1RustProjection, PublishOutcome};
use kcode_k1_rust_transaction::{TransactionError, encode};
use kcode_k1_transaction_id::TxId as RustTxId;
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 = "source-package.k1rust";
const OBJECT_MEDIA_TYPE: &str = "application/vnd.kennedy.k1-rust-source-package.v1";
#[derive(Debug)]
pub enum PublicPackageError {
Absent,
Unsupported(RustCodeDocumentError),
Backend(String),
}
impl Display for PublicPackageError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
Debug::fmt(self, formatter)
}
}
impl Error for PublicPackageError {}
#[derive(Debug)]
pub enum RustCodeKtoolServiceError {
State(String),
CodingInitialization(String),
Coding(RustCodingError),
SourceEncoding(TransactionError),
Object(String),
CheckMismatch,
}
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 load_document(&self, id: &LibraryId) -> Result<RustCodeDocument, PublicPackageError> {
match self.projection.load(id) {
Ok(Some(package)) => RustCodeDocument::from_source_package(package)
.map_err(PublicPackageError::Unsupported),
Ok(None) => Err(PublicPackageError::Absent),
Err(error) => Err(PublicPackageError::Backend(error)),
}
}
pub fn identity_exists(&self, id: &LibraryId) -> Result<bool, PublicPackageError> {
self.projection
.load(id)
.map(|package| package.is_some())
.map_err(PublicPackageError::Backend)
}
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);
}
let coding = self.coding(authenticated)?;
let mut coding = coding
.lock()
.map_err(|_| RustCodeKtoolServiceError::State("user coding mutex poisoned".into()))?;
let bytes = encode(&package).map_err(RustCodeKtoolServiceError::SourceEncoding)?;
self.objects
.save(OBJECT_FILENAME, OBJECT_MEDIA_TYPE, "", &bytes)
.map_err(RustCodeKtoolServiceError::Object)?;
let gate = PublishGate {
authenticated,
groups: &self.groups,
};
coding
.publish_checked(&package, &checked.verified, &gate)
.map_err(RustCodeKtoolServiceError::Coding)
}
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(),
)),
}
}
}
struct PublishGate<'a> {
authenticated: &'a AccessContext,
groups: &'a K1Groups,
}
impl RustPublishAuthorization for PublishGate<'_> {
fn authorize_publish(&self, family: &LibraryFamily) -> Result<bool, String> {
let authority_id = family.authority();
let authority = authority_id.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))
}
}