use kcode_k1_access::AccessContext;
use kcode_k1_groups::K1Groups;
use kcode_k1_objects::K1Objects;
use kcode_k1_web_code_authority::authorize_workspace;
pub use kcode_k1_web_code_document::{CodeDocument, Language};
use kcode_k1_web_code_runtime::{K1WebCodeRuntime, RuntimeError};
pub use kcode_k1_web_code_runtime::{
RuntimeConfig as ServiceConfig, RuntimeRevisions as ServiceRevisions,
};
pub use kcode_k1_web_code_session::{CheckEvidence, DocumentPart, OpenedLibrary};
use kcode_k1_web_code_session::{SessionError, SessionScope};
pub use kcode_k1_web_coding::{CheckOutcome, PublishResult};
use kcode_k1_web_coding::{K1WebCoding, WebCodingError};
use kcode_k1_web_package::{SourcePackage, WebFamily, WebId};
use kcode_k1_web_projection::K1WebProjection;
use kcode_k1_web_source_object::{CaptureStateError, SourceObjectCapture};
use std::fmt::{Display, Formatter};
use std::sync::Arc;
pub use kcode_k1_objects::TxId;
const EMPTY_DOCUMENTATION: &[u8] = b"<!-- k1-web/v1\n{\"dependencies\":[]}\n-->\n";
type ServiceResult<T> = Result<T, ServiceError>;
pub struct FreshCheck {
pub outcome: CheckOutcome,
evidence: CheckEvidence,
}
impl FreshCheck {
pub fn evidence(&self) -> &CheckEvidence {
&self.evidence
}
}
pub struct PublishCompletion {
pub source_object: TxId,
pub publication: PublishResult,
pub precheck: Option<CheckOutcome>,
pub evidence: CheckEvidence,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ServiceFailure {
State,
Authorization,
WorkspaceDenied,
SourceUnavailable,
LanguageRequired,
LanguageMismatch,
ForeignHandle,
StaleSource,
InvalidDocument,
CheckFailed,
SourcePreservation,
PublicReleaseDenied,
Publication,
Dependency,
}
#[derive(Debug)]
pub struct ServiceError {
failure: ServiceFailure,
message: String,
source_object: Option<TxId>,
session_error: Option<SessionError>,
}
impl ServiceError {
pub fn failure(&self) -> ServiceFailure {
self.failure
}
pub fn source_object(&self) -> Option<TxId> {
self.source_object
}
fn new(failure: ServiceFailure, message: impl Into<String>) -> Self {
Self {
failure,
message: message.into(),
source_object: None,
session_error: None,
}
}
fn from_session(error: SessionError) -> Self {
let failure = match &error {
SessionError::ForeignHandle => ServiceFailure::ForeignHandle,
SessionError::StaleSource => ServiceFailure::StaleSource,
SessionError::InvalidDocument(_) => ServiceFailure::InvalidDocument,
};
let message = match &error {
SessionError::ForeignHandle => {
"opened library does not belong to this service and user".to_owned()
}
SessionError::StaleSource => {
"opened library is not the current exact source".to_owned()
}
SessionError::InvalidDocument(message) => message.clone(),
};
Self {
failure,
message,
source_object: None,
session_error: Some(error),
}
}
fn with_source_object(mut self, source_object: Option<TxId>) -> Self {
self.source_object = source_object;
self
}
}
impl Display for ServiceError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.message)
}
}
impl std::error::Error for ServiceError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.session_error
.as_ref()
.map(|error| error as &(dyn std::error::Error + 'static))
}
}
struct Inner {
runtime: K1WebCodeRuntime,
groups: Arc<K1Groups>,
objects: Arc<K1Objects>,
session: SessionScope,
}
#[derive(Clone)]
pub struct K1WebCodeKtoolService {
inner: Arc<Inner>,
}
impl K1WebCodeKtoolService {
pub fn new(
config: ServiceConfig,
groups: Arc<K1Groups>,
objects: Arc<K1Objects>,
projection: Arc<K1WebProjection>,
) -> Self {
Self {
inner: Arc::new(Inner {
runtime: K1WebCodeRuntime::new(config, projection),
groups,
objects,
session: SessionScope::new(),
}),
}
}
pub fn docs(&self, context: &AccessContext, id: &WebId) -> ServiceResult<Option<String>> {
let user = user_bytes(context);
self.with_coding(user, |coding| {
let source = coding
.view(id, &|family| self.authorize(context, family))
.map_err(service_coding_error)?;
source
.map(|source| {
let opened = self
.inner
.session
.recover(user, source)
.map_err(ServiceError::from_session)?;
String::from_utf8(opened.document().documentation().to_vec()).map_err(|error| {
ServiceError::new(ServiceFailure::InvalidDocument, error.to_string())
})
})
.transpose()
})
}
pub fn open(
&self,
context: &AccessContext,
id: &WebId,
language: Option<Language>,
) -> ServiceResult<OpenedLibrary> {
let user = user_bytes(context);
self.with_coding(user, |coding| {
self.require_authorized(context, id.family())?;
let current = coding
.view(id, &|family| self.authorize(context, family))
.map_err(service_coding_error)?;
let candidate = match current {
Some(source) => {
let recovered = self
.inner
.session
.recover(user, source)
.map_err(ServiceError::from_session)?;
if language.is_some_and(|value| value != recovered.document().language()) {
return Err(ServiceError::new(
ServiceFailure::LanguageMismatch,
"supplied language does not match the current document",
));
}
recovered
.document()
.to_source_package()
.map_err(document_error)?
}
None => CodeDocument::new(
id.clone(),
EMPTY_DOCUMENTATION.to_vec(),
language.ok_or_else(|| {
ServiceError::new(
ServiceFailure::LanguageRequired,
"language is required for an absent library",
)
})?,
Vec::new(),
)
.map_err(document_error)?
.to_source_package()
.map_err(document_error)?,
};
let written = coding
.write(&candidate, &|family| self.authorize(context, family))
.map_err(service_coding_error)?;
self.inner
.session
.refresh(user, written)
.map_err(ServiceError::from_session)
})
}
pub fn validate_current(
&self,
context: &AccessContext,
opened: &OpenedLibrary,
) -> ServiceResult<()> {
let user = user_bytes(context);
self.with_coding(user, |coding| {
self.current_source(context, coding, user, opened)?;
Ok(())
})
}
pub fn overwrite(
&self,
context: &AccessContext,
opened: &OpenedLibrary,
part: DocumentPart,
contents: String,
) -> ServiceResult<OpenedLibrary> {
let user = user_bytes(context);
self.with_coding(user, |coding| {
let current = self.current_source(context, coding, user, opened)?;
let candidate = self
.inner
.session
.replacement(user, opened, current.as_ref(), part, contents)
.map_err(ServiceError::from_session)?;
let written = coding
.write(&candidate, &|family| self.authorize(context, family))
.map_err(service_coding_error)?;
self.inner
.session
.refresh(user, written)
.map_err(ServiceError::from_session)
})
}
pub fn check(
&self,
context: &AccessContext,
opened: &OpenedLibrary,
) -> ServiceResult<FreshCheck> {
let user = user_bytes(context);
self.with_coding(user, |coding| {
let current = self.current_source(context, coding, user, opened)?;
let outcome = coding
.check_fresh(opened.document().id(), &|family| {
self.authorize(context, family)
})
.map_err(service_coding_error)?;
let evidence = self
.inner
.session
.mint_evidence(user, opened, current.as_ref())
.map_err(ServiceError::from_session)?;
Ok(FreshCheck { outcome, evidence })
})
}
pub fn publish(
&self,
context: &AccessContext,
opened: &OpenedLibrary,
evidence: Option<&CheckEvidence>,
) -> ServiceResult<PublishCompletion> {
let user = user_bytes(context);
self.with_coding(user, |coding| {
let current = self.current_source(context, coding, user, opened)?;
let evidence_matches = evidence
.map(|value| {
self.inner
.session
.evidence_matches(user, opened, current.as_ref(), value)
.map_err(ServiceError::from_session)
})
.transpose()?
.unwrap_or(false);
let precheck = if evidence_matches {
None
} else {
Some(
coding
.check_fresh(opened.document().id(), &|family| {
self.authorize(context, family)
})
.map_err(service_coding_error)?,
)
};
let completion_evidence = self
.inner
.session
.mint_evidence(user, opened, current.as_ref())
.map_err(ServiceError::from_session)?;
let source_object = SourceObjectCapture::new(self.inner.objects.as_ref());
let publication = coding.publish_with_source_preservation(
opened.document().id(),
&|family| self.authorize(context, family),
&|source, _digest| source_object.preserve(source),
&|source, _digest| self.authorize(context, source.id().family()),
);
let publication = match publication {
Ok(value) => value,
Err(error) => {
let captured = source_object.captured();
return Err(service_coding_error(error).with_source_object(captured));
}
};
let source_object = source_object.finish().map_err(source_capture_error)?;
Ok(PublishCompletion {
source_object,
publication,
precheck,
evidence: completion_evidence,
})
})
}
fn with_coding<T>(
&self,
user: [u8; 12],
operation: impl FnOnce(&mut K1WebCoding) -> ServiceResult<T>,
) -> ServiceResult<T> {
self.inner
.runtime
.with_user(user, |coding| Ok(operation(coding)))
.map_err(service_runtime_error)?
}
fn authorize(&self, context: &AccessContext, family: &WebFamily) -> Result<bool, String> {
authorize_workspace(context, &self.inner.groups, family)
}
fn require_authorized(&self, context: &AccessContext, family: &WebFamily) -> ServiceResult<()> {
match self.authorize(context, family) {
Ok(true) => Ok(()),
Ok(false) => Err(ServiceError::new(
ServiceFailure::WorkspaceDenied,
"workspace is denied",
)),
Err(message) => Err(ServiceError::new(ServiceFailure::Authorization, message)),
}
}
fn current_source(
&self,
context: &AccessContext,
coding: &K1WebCoding,
user: [u8; 12],
opened: &OpenedLibrary,
) -> ServiceResult<Arc<SourcePackage>> {
let current = coding
.view(opened.document().id(), &|family| {
self.authorize(context, family)
})
.map_err(service_coding_error)?
.ok_or_else(|| ServiceError::from_session(SessionError::StaleSource))?;
self.inner
.session
.validate(user, opened, current.as_ref())
.map_err(ServiceError::from_session)?;
Ok(current)
}
}
fn user_bytes(context: &AccessContext) -> [u8; 12] {
*context.user().as_tx_id().as_bytes()
}
fn document_error(error: impl Display) -> ServiceError {
ServiceError::new(ServiceFailure::InvalidDocument, error.to_string())
}
fn source_capture_error(error: CaptureStateError) -> ServiceError {
let message = match error {
CaptureStateError::Poisoned => "source object capture mutex poisoned",
CaptureStateError::Missing => "publication completed without a captured source object",
};
ServiceError::new(ServiceFailure::State, message)
}
fn service_runtime_error(error: RuntimeError) -> ServiceError {
match error {
RuntimeError::Coding(error) => service_coding_error(error),
RuntimeError::LockPoisoned(message) => {
let message = if message == "runtime slot map mutex poisoned" {
"service slot map mutex poisoned".to_owned()
} else {
message
};
ServiceError::new(ServiceFailure::State, message)
}
}
}
fn service_coding_error(error: WebCodingError) -> ServiceError {
let failure = match &error {
WebCodingError::Authorization(_) => ServiceFailure::Authorization,
WebCodingError::WorkspaceDenied => ServiceFailure::WorkspaceDenied,
WebCodingError::CandidateUnavailable => ServiceFailure::SourceUnavailable,
WebCodingError::CheckFailed(_) => ServiceFailure::CheckFailed,
WebCodingError::SourcePreservation(_) => ServiceFailure::SourcePreservation,
WebCodingError::PublicReleaseDenied => ServiceFailure::PublicReleaseDenied,
WebCodingError::Projection(_) | WebCodingError::ProjectionAfterSubmit { .. } => {
ServiceFailure::Publication
}
WebCodingError::State(_)
| WebCodingError::Cache(_)
| WebCodingError::DependencyUnavailable
| WebCodingError::Podman(_) => ServiceFailure::Dependency,
};
ServiceError::new(failure, error.to_string())
}