#![forbid(unsafe_code)]
use kcode_k1_access::{AccessContext, Authority};
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::{DocumentPart, OpenedLibrary, OpenedSource};
use kcode_k1_web_code_session::{SessionError, SessionScope};
pub use kcode_k1_web_code_workspace::TxId as WorkspaceId;
use kcode_k1_web_code_workspace::{K1WebCodeWorkspace, Workspace};
pub use kcode_k1_web_coding::{CheckOutcome, PublishResult};
use kcode_k1_web_coding::{K1WebCoding, WebCodingError};
use kcode_k1_web_package::{AuthorityId, SourcePackage, WebFamily, WebId};
use kcode_k1_web_projection::K1WebProjection;
use kcode_k1_web_source_object::{CaptureStateError, SourceObjectCapture};
use semver::Version;
use std::{
fmt::{Display, Formatter},
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>;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DocsResult {
pub version: String,
pub documentation: String,
}
pub struct PublishCompletion {
pub source_object: TxId,
pub publication: PublishResult,
pub check: CheckOutcome,
pub opened: OpenedLibrary,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ServiceFailure {
State,
Authorization,
WorkspaceDenied,
FamilyExists,
SourceUnavailable,
VersionUsed,
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 const fn failure(&self) -> ServiceFailure {
self.failure
}
pub const 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 session(error: SessionError) -> Self {
let failure = match error {
SessionError::ForeignHandle => ServiceFailure::ForeignHandle,
SessionError::StaleSource => ServiceFailure::StaleSource,
SessionError::InvalidDocument(_) => ServiceFailure::InvalidDocument,
};
Self {
failure,
message: error.to_string(),
source_object: None,
session_error: Some(error),
}
}
fn with_object(mut self, object: Option<TxId>) -> Self {
self.source_object = 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 _)
}
}
struct Inner {
runtime: K1WebCodeRuntime,
groups: Arc<K1Groups>,
objects: Arc<K1Objects>,
public: Arc<K1WebProjection>,
workspaces: Arc<K1WebCodeWorkspace>,
session: SessionScope,
}
#[derive(Clone)]
pub struct K1WebCodeKtoolService {
inner: Arc<Inner>,
}
impl K1WebCodeKtoolService {
pub fn new(
config: ServiceConfig,
groups: Arc<K1Groups>,
objects: Arc<K1Objects>,
public: Arc<K1WebProjection>,
workspaces: Arc<K1WebCodeWorkspace>,
) -> Self {
Self {
inner: Arc::new(Inner {
runtime: K1WebCodeRuntime::new(config, public.clone()),
groups,
objects,
public,
workspaces,
session: SessionScope::new(),
}),
}
}
pub fn create(
&self,
context: &AccessContext,
authority: Authority,
library: &str,
language: Language,
) -> ServiceResult<OpenedLibrary> {
let family = family(authority, library)?;
self.require_authorized(context, &family)?;
let snapshot = self.inner.public.snapshot().map_err(dependency)?;
let workspace_exists = self
.inner
.workspaces
.latest(&family)
.map_err(dependency)?
.is_some();
if !snapshot.versions(&family).is_empty() || workspace_exists {
return Err(ServiceError::new(
ServiceFailure::FamilyExists,
"library already exists; use WebCodeOpen",
));
}
let document =
CodeDocument::new(family, EMPTY_DOCUMENTATION.to_vec(), language, Vec::new())
.map_err(document_error)?;
let workspace = self
.inner
.workspaces
.create(document)
.map_err(workspace_error)?;
self.open_workspace(authority, workspace)
}
pub fn docs(
&self,
context: &AccessContext,
authority: Authority,
library: &str,
version: Option<VersionChoice>,
) -> ServiceResult<Option<DocsResult>> {
let family = family(authority, library)?;
let resolved = match version {
Some(VersionChoice::Published(version)) => self.published(&family, &version)?,
Some(VersionChoice::Unpublished(id)) => {
self.require_authorized(context, &family)?;
self.workspace(&family, id)?
}
None => self.latest_published(&family)?,
};
resolved
.map(|value| {
Ok(DocsResult {
version: value.source.text(),
documentation: String::from_utf8(value.document.documentation().to_vec())
.map_err(document_error)?,
})
})
.transpose()
}
pub fn open(
&self,
context: &AccessContext,
authority: Authority,
library: &str,
version: Option<VersionChoice>,
) -> ServiceResult<Option<OpenedLibrary>> {
let family = family(authority, library)?;
let resolved = match version {
Some(VersionChoice::Published(version)) => self.published(&family, &version)?,
Some(VersionChoice::Unpublished(id)) => {
self.require_authorized(context, &family)?;
self.workspace(&family, id)?
}
None => self.default_open(context, &family)?,
};
resolved
.map(|value| {
self.inner
.session
.open(authority_bytes(authority), value.document, value.source)
.map_err(ServiceError::session)
})
.transpose()
}
pub fn validate_current(
&self,
context: &AccessContext,
authority: Authority,
opened: &OpenedLibrary,
) -> ServiceResult<()> {
self.current(context, authority, opened).map(|_| ())
}
pub fn overwrite(
&self,
context: &AccessContext,
authority: Authority,
opened: &OpenedLibrary,
part: DocumentPart,
contents: String,
) -> ServiceResult<OpenedLibrary> {
let current = self.current(context, authority, opened)?;
let replacement = self
.inner
.session
.replacement(
authority_bytes(authority),
opened,
¤t.document,
¤t.source,
part,
contents,
)
.map_err(ServiceError::session)?;
let workspace = match current.source {
OpenedSource::Published(_) => self
.inner
.workspaces
.branch(replacement)
.map_err(workspace_error)?,
OpenedSource::Unpublished { .. } if self.is_published(¤t.document)? => self
.inner
.workspaces
.branch(replacement)
.map_err(workspace_error)?,
OpenedSource::Unpublished { id, revision } => self
.inner
.workspaces
.overwrite(id, revision, replacement)
.map_err(workspace_error)?,
};
self.open_workspace(authority, workspace)
}
pub fn check(
&self,
context: &AccessContext,
authority: Authority,
opened: &OpenedLibrary,
) -> ServiceResult<CheckOutcome> {
let current = self.current(context, authority, opened)?;
let package = current
.document
.to_source_package(check_version(¤t.source))
.map_err(document_error)?;
self.with_coding(context, |coding| {
coding
.write(&package, &|family| self.authorize(context, family))
.map_err(coding_error)?;
coding
.check_fresh(package.id(), &|family| self.authorize(context, family))
.map_err(coding_error)
})
}
pub fn publish(
&self,
context: &AccessContext,
authority: Authority,
opened: &OpenedLibrary,
version: Version,
) -> ServiceResult<PublishCompletion> {
let current = self.current(context, authority, opened)?;
let id = WebId::new(current.document.family().clone(), version.clone())
.map_err(document_error)?;
if self.inner.public.load(&id).map_err(dependency)?.is_some() {
return Err(ServiceError::new(
ServiceFailure::VersionUsed,
"version is already published",
));
}
let package = current
.document
.to_source_package(version.clone())
.map_err(document_error)?;
let (check, publication, source_object) = self.with_coding(context, |coding| {
coding
.write(&package, &|family| self.authorize(context, family))
.map_err(coding_error)?;
let check = coding
.check_fresh(package.id(), &|family| self.authorize(context, family))
.map_err(coding_error)?;
let capture = SourceObjectCapture::new(self.inner.objects.as_ref());
let publication = coding.publish_with_source_preservation(
package.id(),
&|family| self.authorize(context, family),
&|source, _| capture.preserve(source),
&|source, _| self.authorize(context, source.id().family()),
);
let publication =
publication.map_err(|error| coding_error(error).with_object(capture.captured()))?;
let object = capture.finish().map_err(capture_error)?;
Ok((check, publication, object))
})?;
let opened = self
.inner
.session
.refresh(
authority_bytes(authority),
current.document,
OpenedSource::Published(version),
)
.map_err(ServiceError::session)?;
Ok(PublishCompletion {
source_object,
publication,
check,
opened,
})
}
fn current(
&self,
context: &AccessContext,
authority: Authority,
opened: &OpenedLibrary,
) -> ServiceResult<Resolved> {
let family = opened.document().family();
if family.authority().transaction_id().as_bytes() != &authority_bytes(authority) {
return Err(ServiceError::session(SessionError::ForeignHandle));
}
self.require_authorized(context, family)?;
let resolved = match opened.source() {
OpenedSource::Published(version) => self.published(family, version)?,
OpenedSource::Unpublished { id, .. } => self.workspace(family, *id)?,
}
.ok_or_else(|| ServiceError::session(SessionError::StaleSource))?;
self.inner
.session
.validate(
authority_bytes(authority),
opened,
&resolved.document,
&resolved.source,
)
.map_err(ServiceError::session)?;
Ok(resolved)
}
fn default_open(
&self,
context: &AccessContext,
family: &WebFamily,
) -> ServiceResult<Option<Resolved>> {
if let Some(workspace) = self.inner.workspaces.latest(family).map_err(dependency)?
&& !self.is_published(workspace.document())?
{
self.require_authorized(context, family)?;
return Ok(Some(from_workspace(workspace)));
}
self.latest_published(family)
}
fn latest_published(&self, family: &WebFamily) -> ServiceResult<Option<Resolved>> {
let snapshot = self.inner.public.snapshot().map_err(dependency)?;
let Some(version) = snapshot.versions(family).pop() else {
return Ok(None);
};
snapshot
.load(&WebId::new(family.clone(), version.clone()).map_err(document_error)?)
.map(|source| from_public(source, version))
.transpose()
}
fn published(&self, family: &WebFamily, version: &Version) -> ServiceResult<Option<Resolved>> {
let id = WebId::new(family.clone(), version.clone()).map_err(document_error)?;
self.inner
.public
.load(&id)
.map_err(dependency)?
.map(|source| from_public(source, version.clone()))
.transpose()
}
fn workspace(&self, family: &WebFamily, id: WorkspaceId) -> ServiceResult<Option<Resolved>> {
Ok(self
.inner
.workspaces
.get(id)
.map_err(dependency)?
.filter(|value| value.document().family() == family)
.map(from_workspace))
}
fn is_published(&self, document: &CodeDocument) -> ServiceResult<bool> {
let snapshot = self.inner.public.snapshot().map_err(dependency)?;
for version in snapshot.versions(document.family()) {
let id = WebId::new(document.family().clone(), version).map_err(document_error)?;
if let Some(source) = snapshot.load(&id)
&& CodeDocument::from_source_package(&source).map_err(document_error)? == *document
{
return Ok(true);
}
}
Ok(false)
}
fn open_workspace(
&self,
authority: Authority,
workspace: Workspace,
) -> ServiceResult<OpenedLibrary> {
let source = OpenedSource::Unpublished {
id: workspace.id(),
revision: workspace.revision(),
};
self.inner
.session
.refresh(
authority_bytes(authority),
workspace.document().clone(),
source,
)
.map_err(ServiceError::session)
}
fn with_coding<T>(
&self,
context: &AccessContext,
operation: impl FnOnce(&mut K1WebCoding) -> ServiceResult<T>,
) -> ServiceResult<T> {
self.inner
.runtime
.with_user(*context.user().as_tx_id().as_bytes(), |coding| {
Ok(operation(coding))
})
.map_err(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(error) => Err(ServiceError::new(ServiceFailure::Authorization, error)),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum VersionChoice {
Published(Version),
Unpublished(WorkspaceId),
}
struct Resolved {
document: CodeDocument,
source: OpenedSource,
}
fn from_workspace(value: Workspace) -> Resolved {
let source = OpenedSource::Unpublished {
id: value.id(),
revision: value.revision(),
};
Resolved {
document: value.document().clone(),
source,
}
}
fn from_public(source: Arc<SourcePackage>, version: Version) -> ServiceResult<Resolved> {
Ok(Resolved {
document: CodeDocument::from_source_package(&source).map_err(document_error)?,
source: OpenedSource::Published(version),
})
}
fn family(authority: Authority, library: &str) -> ServiceResult<WebFamily> {
WebFamily::new(
AuthorityId::new(WorkspaceId::from_bytes(authority_bytes(authority))),
library,
)
.map_err(document_error)
}
fn authority_bytes(authority: Authority) -> [u8; 12] {
match authority {
Authority::User(value) => *value.as_tx_id().as_bytes(),
Authority::Group(value) => *value.txid().as_bytes(),
}
}
fn check_version(source: &OpenedSource) -> Version {
match source {
OpenedSource::Published(value) => value.clone(),
OpenedSource::Unpublished { id, .. } => {
let bytes = id.as_bytes();
Version::new(
u32::from_be_bytes(bytes[0..4].try_into().expect("four bytes")) as u64,
u32::from_be_bytes(bytes[4..8].try_into().expect("four bytes")) as u64,
u32::from_be_bytes(bytes[8..12].try_into().expect("four bytes")) as u64,
)
}
}
}
fn document_error(error: impl Display) -> ServiceError {
ServiceError::new(ServiceFailure::InvalidDocument, error.to_string())
}
fn dependency(error: impl Display) -> ServiceError {
ServiceError::new(ServiceFailure::Dependency, error.to_string())
}
fn workspace_error(error: impl Display) -> ServiceError {
ServiceError::new(ServiceFailure::Dependency, error.to_string())
}
fn runtime_error(error: RuntimeError) -> ServiceError {
match error {
RuntimeError::Coding(error) => coding_error(error),
RuntimeError::LockPoisoned(message) => ServiceError::new(ServiceFailure::State, message),
}
}
fn 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
}
_ => ServiceFailure::Dependency,
};
ServiceError::new(failure, error.to_string())
}
fn 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)
}